Android integrates fluent | and interaction

Posted by plasko on Fri, 11 Feb 2022 12:20:02 +0100

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

  1. 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:

  1. 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

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...