preface
Flutter has been used for some time, and the development experience is still very good. However, when we officially use flutter, we rarely create a pure flutter project, but need to write flutter in an integrated way in previous projects. This article will focus on how to integrate fluent into Android projects and how to interact between them.
Integrate the fluent project in the Android project
First, we need to find an android project to integrate Fluuter based on this. Let's take a look at the specific steps
- Creating a shuttle module Use the following command in the Terminal of Android studio
flutter create -t module flutter_module
Where my_ Fluent is the module name. After the command is completed, a new folder, flitter, will be generated in the project directory_ module Or you can directly use AS to create a fluent module.
2. Put two items under one folder This step is mainly to facilitate management, and can be uploaded to git separately for development. Not in a directory.
3. Execute fluent build AAR Open the shutter module and execute the shutter build AAR command. After execution, it is displayed as follows:
- Complete the four items in the screenshot above The four projects in the above screenshot need to be completed in android code
repositories { //... maven { url 'D:\\android\\project\\example\\flutter_module\\build\\host\\outputs\\repo' } maven { url "https://storage.googleapis.com/download.flutter.io" } }
The repositories of new projects need to be configured in setting In gradle. The url above is fluuter_modlue's path.
dependencies { //..... debugImplementation 'com.lv.example.flutter_module:flutter_debug:1.0' profileImplementation 'com.lv.example.flutter_module:flutter_profile:1.0' releaseImplementation 'com.lv.example.flutter_module:flutter_release:1.0' }
buildTypes { profile { initWith debug } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }
5. Synchronization project Synchronize the project to see if there is any error reported. If so, check the problem
6. Add FlutterActivity At adnroidmanifest Add FlutterActivity to XML
<activity android:name="io.flutter.embedding.android.FlutterActivity" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" />
7. Jump
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<View>(R.id.start).setOnClickListener { startActivity( FlutterActivity.createDefaultIntent(this) ) } } }
8. The effect is as follows:
Interaction between fluent and Android
Android calls up the shutter page
In the above code, there is already a code to open the shutter page, as shown below:
startActivity(FlutterActivity.createDefaultIntent(this)) Copy code
However, when you run the code, you will find that this way will start very slowly. Let's take a look at a way to pre initialize the Flutter
class MainActivity : AppCompatActivity() { private val flutterEngine by lazy { initFlutterEngine() }; override fun onCreate(savedInstanceState: Bundle?) { ...// findViewById<View>(R.id.start).setOnClickListener { startActivity( FlutterActivity.withCachedEngine("default_engine_id").build(this) ) } } private fun initFlutterEngine(): FlutterEngine { //Create a fluent engine val flutterEngine = FlutterEngine(this) //Specify the flitter page to jump to flutterEngine.navigationChannel.setInitialRoute("main") flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault()) //Here is a cache, which can be executed at an appropriate time. For example, in app, preload is performed before jump val flutterEngineCache = FlutterEngineCache.getInstance(); flutterEngineCache.put("default_engine_id", flutterEngine) //The above code is usually invoked before jumping, which can speed up the jump tree. return flutterEngine } override fun onDestroy() { super.onDestroy() flutterEngine.destroy() } } Copy code
The code of Flutter is as follows:
void main() => runApp(getRouter(window.defaultRouteName)); Widget getRouter(String name) { switch (name) { case "main": return const MyApp(); default: return MaterialApp( title: "Flutter Demo", theme: ThemeData( primarySwatch: Colors.blue, ), home: Container( alignment: Alignment.center, child: Text("not font page $name}"), ), ); } } Copy code
The effect is as follows:
It can be found that the jump speed is significantly accelerated.
It should be noted that fluuter is not modified_ After the code in the model is re run, the page will change. In the android project, the code of fluent exists in the form of an aar package. Therefore, after the code of FLUENT is updated, you need to re execute the command of fluent build aar and re type an aar package. Of course, this does not mean that you should operate like this every time. In the normal development process, you can directly run fluent_ Module. Execute the command when it needs to be closed.
When using a cached FlutterEngine, the FlutterEngine has a longer lifetime than any FlutterActivity or FlutterFragment that displays it. Remember that Dart code starts executing immediately after you warm up the FlutterEngine and continues executing after your FlutterActivity/FlutterFragment is destroyed. To stop execution and clear resources, get the FlutterEngine from the FlutterEngine cache and use the FlutterEngine Destroy() destroys the FlutterEngine.
Finally, if you want to test performance, use the release version
Jump to the shuttle with parameters
If you need to carry parameters during jump, you only need to splice parameters behind the route, as shown below:
flutterEngine.navigationChannel.setInitialRoute("main?{\"name\":\"345\"}") Copy code
Here will be used for routing and parameters? Separated, parameters are passed in json format.
At the Fletter end, through the window Defaultroutename gets the route + parameter. We just need to analyze:
String url = window.defaultRouteName; // route name String route = url.indexOf('?') == -1 ? url : url.substring(0, url.indexOf('?')); // Parameter Json string String paramsJson = url.indexOf('?') == -1 ? '{}' : url.substring(url.indexOf('?') + 1); // Analytical parameters Map<String, dynamic> params = json.decode(paramsJson); Copy code
The jump parameters can be obtained through the above code
Launch FlutterActivity transparently
startActivity( FlutterActivity.withCachedEngine("default_engine_id") .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent) .build(this) ) Copy code
Launch the FlutterActivity in a translucent manner
1. A theme attribute is required to render translucent effect
<style name="MyTheme" parent="@style/MyParentTheme"> <item name="android:windowIsTranslucent">true</item> </style> Copy code
2. Apply the theme to fluteractivity
<activity android:name="io.flutter.embedding.android.FlutterActivity" android:theme="@style/MyTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" /> Copy code
This allows the FlutterActivity to support translucency
Android embedded FlutterFragment
A FlutterFragment is displayed on the Android page. The basic operations are as follows:
class MainActivity : AppCompatActivity() { //Define a tag string to represent the FragmentManager of the FlutterFragment activity in it. This value can be any value you want. private val tagFlutterFragment = "flutter_fragment" private var flutterFragment: FlutterFragment? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val flutterEngine = initFlutterEngine() findViewById<View>(R.id.start).setOnClickListener { //This is not the first attempt to find an existing fragment () flutterFragment = supportFragmentManager.findFragmentByTag(tagFlutterFragment) as FlutterFragment? //Create a FlutterFragment if (flutterFragment == null) flutterFragment = FlutterFragment .withCachedEngine("default_engine_id") .build() //Load FlutterFragment supportFragmentManager .beginTransaction() .add(R.id.layout, flutterFragment!!, tagFlutterFragment) .commit() } } private fun initFlutterEngine(): FlutterEngine { //Create a fluent engine val flutterEngine = FlutterEngine(this) //Specify the flitter page to jump to flutterEngine.navigationChannel.setInitialRoute("main") flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault()) //Here is a cache, which can be executed at an appropriate time. For example, in app, preload is performed before jump val flutterEngineCache = FlutterEngineCache.getInstance(); flutterEngineCache.put("default_engine_id", flutterEngine) //The above code is usually invoked before jumping, which can speed up the jump tree. return flutterEngine } override fun onPostResume() { super.onPostResume() flutterFragment?.onPostResume() } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) flutterFragment?.onNewIntent(intent) } override fun onBackPressed() { super.onBackPressed() flutterFragment?.onBackPressed() } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) flutterFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults) } override fun onUserLeaveHint() { super.onUserLeaveHint() flutterFragment?.onUserLeaveHint() } override fun onTrimMemory(level: Int) { super.onTrimMemory(level) flutterFragment?.onTrimMemory(level) } override fun onDestroy() { super.onDestroy() flutterEngine.destroy() } } Copy code
The above code directly opens FlutterFragmetn by initializing the engine, which has the advantage of loading more blocks.
It should be noted that if you want to achieve all the expected behaviors of Flutter, you must forward these signals to the FlutterFragment, which is why so many methods are re used above.
Run the FlutterFragment from the specified entry point
Similar to different initial routes, different flutterfragment s may want to execute different Dart entry points. In a typical fluent application, there is only one Dart entry point: main(), but you can define other entry points.
The FlutterFragment supports the specification of the required Dart entry point for a given Flutter experience. To specify an entry point, build the FlutterFragment as follows:
FlutterFragment.withNewEngine() .dartEntrypoint("newMain") .build() Copy code
The FlutterFragment will launch an entry point named newMian.
The configuration of the shuttle end is as follows:
void main() => runApp(MyApp(window.defaultRouteName)); void newMain() => runApp(NewMainApp()); Copy code
It should be noted that it must be configured in main Dart file.
When the FlutterFragment uses the cache, the Dart entry point attribute is invalid, so the cache cannot be used after the entry is specified.
Controls the rendering mode of the FlutterFragment
Fluent can use SufaceView to render his content, or TextureView.
FlutterFragment uses SurfaceView by default. Its new capability is significantly higher than TextureView, but SufaceView can no longer cross in the Android View hierarchy. SurfaceView must be the lowest view or the top view.
In addition, in versions prior to Android N, SurfaceView cannot use animation because their layout rendering is different from other parts of the View hierarchy.
Then you need to use TextureView instead of SurfaceView. Select TextureView by using RenderMode to build the FlutterFragment, as shown below:
val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id") .renderMode(FlutterView.RenderMode.texture) .build() Copy code
FlutterFragment with transparency
By default, FlutterFragment uses SurfaceView to render an opaque background. The background is black for any pixel that is not drawn by Flutter. For performance reasons, rendering with an opaque background is the preferred rendering mode. Fluent rendering with transparency on Android can have a negative impact on performance. However, there are many designs that need to display transparent pixels in the Flutter experience, which will be displayed in the underlying Android UI. Therefore, Flutter supports translucency in FlutterFragment
Both SurfaceView and TextureView support transparency. However, when SurfaceView is instructed to render transparently, it positions itself on a z-index higher than all other Android views, which means it appears on top of all other views. This is a limitation of SurfaceView. If it is acceptable to render your Flutter experience on top of everything else, the default RenderMode of the surface of the FlutterFragment is the RenderMode you should use. However, if you need to display the Android view above and below the fluent experience, you must specify the RenderMode texture. have
The usage is as follows:
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id") .transparencyMode(FlutterView.TransparencyMode.transparent) .build(); Copy code
Relationship between FlutterFragment and its Activity
Some apps choose to use Fragments as the entire Android screen. In these applications, it is reasonable to use Fragment to control the system chrome, such as Android's status bar, navigation bar and direction.
In other applications, fragments are used to represent only part of the UI. FlutterFragment can be used to implement the interior of drawers, video players or single cards. In these cases, it is inappropriate for the FlutterFragment to affect the Android system chrome, because there are other UI fragments in the same Window.
FlutterFragment has a concept that can help distinguish between situations where FlutterFragment should be able to control its host Activity and situations where FlutterFragment should only affect its own behavior. To prevent the FlutterFragment from exposing its Activity to the Flutter plug-in and prevent the Flutter from controlling the system UI of the Activity, please use the shouldAttachEngineToActivity() method in the Builder of the FlutterFragment, as shown below:
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id") //Whether this FlutterFragment should automatically attach its Activity as the control surface of its FlutterEngine. .shouldAttachEngineToActivity(false) .build(); Copy code
Flutter and Android communication
Before communicating, let's first introduce Platform Channel, which is a tool for Flutter and native communication. There are three types:
- BaseicMessageChannel: it is used to transfer strings and semi-structured information. It can be used by the Flutter and the platform for message data exchange.
- MethodChannel: it is used to transfer method invocation. It can be used when direct method invocation is carried out at the fluent and platform end
- EventChannel: it can be used for the communication of user data stream, event monitoring and cancellation of Flutter and platform
MethodChannel is the most commonly used in daily development. For the other two, you can consult online articles
Android calls the fluent method
val methodChannel = MethodChannel(flutterEngine.dartExecutor, "com.example.AndroidWithFlutter/native") Copy code
An MtthodChannel is defined in the above code. The first parameter is an interface, which is a tool for communicating with Flutter. The second parameter is name, which is the name of the channel (this name needs to be consistent with that defined in Flutter).
//Call the fluent method methodChannel.invokeMethod("flutterMethod","call Flutter parameter",object : MethodChannel.Result { override fun success(result: Any?) { Log.e("---345--->", "$result"); } override fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?) { Log.e("---345--->", "call Flutter fail"); } override fun notImplemented() {} }) } Copy code
The above code calls the method named flutterMethod in Flutter, the first parameter is the method name, the second is the parameter, the callback is the result of calling and whether the call is successful. Let's take a look at how to define it in fluent:
final _channel = const MethodChannel("com.example.AndroidWithFlutter/native"); @override void initState() { super.initState(); ///Listen for calls from android _channel.setMethodCallHandler((call) async { switch (call.method) { case "flutterMethod": print("Parameters: ${call.arguments}"); break; } return "I am Flutter Return value"; }); } Copy code
In the above code, listen to the call of android end, and then judge which method it is according to the method name.
It should be noted that when calling Flutter, its method can be called even if the page is not opened. This should be because flutterEngine has been cached. dart code will be directly executed in flutterEngine, so it can be called directly. But if the cache is not used when the page jumps. At this time, although it shows that the call is successful, you can't get the corresponding parameters in the past because you don't use the cache and the same object, so you can't. You need to pay attention here.
Fluent calls Android method
Code of fluent end:
void _incrementCounter() { //Call the AndroidMethod method of Android var result = _channel.invokeMapMethod("AndroidMethod", "call Android parameter"); result.then((value) => print('Android Return value :$value')); } Copy code
android client code:
methodChannel.setMethodCallHandler { call, result -> when (call.method) { "AndroidMethod" -> { result.success(mapOf("Android Return value" to "\"I am Android\"")) } else -> { result.success("I am Android,No corresponding method was found") } } } Copy code
It should be noted here that the return value must be map when calling android by fluent. This should be noted;
Shuttle jumps to Android page
In fact, the method channel is used by the flitter to jump to the android page. When you need to jump, the flitter can call android and execute the jump logic on the android side, as shown below:
Code of fluent end:
void _incrementCounter() { //Open native PAGE _channel.invokeMapMethod("jumpToNative"); } Copy code
android client code:
//Listen for flutter calling android methodChannel.setMethodCallHandler { call, result -> when (call.method) { "AndroidMethod" -> { result.success(mapOf("Android Return value" to "\"I am Android\"")) } "jumpToNative" -> { //Jump to login page startActivity(Intent(this, LoginActivity::class.java)) } else -> { result.success("I am Android,No corresponding method was found") } } } Copy code
The renderings are as follows:
Implementation of page return parameter transmission
The implementation method is similar to the above. With the help of MethodChannel, you can use channel to call and pass in the corresponding parameters when the page returns.
Memory usage
After we made a specific observation on the memory and unused memory as follows:
Fluent module not imported:
Import the fluent module:
Start only one cache engine:
Looking at the above picture, we can find that before the introduction, the memory usage was only about 55Mb, while after initializing the fluuter engine, the memory instantly reached 181Mb. And this is the case when a single is initialized.
Let's take a look at the impact of initializing multiple:
initFlutterEngine("init_one") initFlutterEngine("init_two") initFlutterEngine("init_three") private fun initFlutterEngine(id: String): FlutterEngine { //Create a fluent engine val flutterEngine = FlutterEngine(this) //Specify the flitter page to jump to flutterEngine.navigationChannel.setInitialRoute("main") flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault()) //Here is a cache, which can be executed at an appropriate time. For example, in app, preload is performed before jump val flutterEngineCache = FlutterEngineCache.getInstance(); flutterEngineCache.put(id, flutterEngine) //The above code is usually invoked before jumping, which can speed up the jump tree. return flutterEngine } Copy code
The code is as shown above. Let's see the results below:
As you can see, a total of four caches have been initialized and a total of 355Mb has been used. 174Mb more than the previous one, and each additional cache will increase by 60Mb on average.
Through the above verification, it can be concluded that after using fluent, the memory will indeed increase a lot, but it will not cause memory pressure.
Through the comparison of adding cache engines, it is found that each time a cache engine is added, it will increase by about 60Mb.
To sum up:
Generally, there is no problem when using it, but it should be noted that one can be initialized when initializing the engine. You cannot reinitialize the engine every time you open a page.
Project example
- Android side: github.com/LvKang-insi...
- FlutterModule: github.com/LvKang-insi...
reference material
docs.flutter.dev/development...
It's a great honor if this article can help you. If there are mistakes and questions in this article, you are welcome to put forward them
My blog is about to be synchronized to Tencent cloud + community. I invite you to join me: cloud.tencent.com/developer/s...