preface

Flutter has been around for a while and the development experience has been very good, but generally we rarely create a pure Flutter project when we use Flutter formally, rather we need to write Flutter in an integrated way from previous projects. This article will focus on how to integrate Flutter into an Android project and how to interact with Flutter between the two.

Integrate the Flutter project with the Android project

First we need to find an Android project on which to integrate Fluuter. Let’s take a look at the specific steps

  1. Create a Flutter module

    Use the following command in Terminal in AndroidStudio

    flutter create -t module flutter_module
    Copy the code

    My_flutter is the module name. This command will create a new folder, flutter_module, in the project directory

    Alternatively, create a Flutter Module using AS directly.

  2. Place both projects under a folder

    This step is mainly for ease of administration, and can be separately uploaded to Git for ease of development, etc.

    Not in the same directory.

  3. Execute flutter build aar

    Open the Flutter module and execute the Flutter build aar command. The following information is displayed after the command is executed:

  4. Complete the four items in the screenshot above

    All four projects in the screenshot above need to be done 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"}}Copy the code

    The new project repositories will need to be configured in Setting.gradle.

    The url above is the path to fluuter_modlue.

    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'
    }
    Copy the code
    buildTypes {
        profile {
            initWith debug
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}Copy the code
  5. Synchronous project

    Synchronize the project to see if there is any error, and troubleshoot the problem if there is

  6. Add FlutterActivity

    Add FlutterActivity to adnroidmanifest.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" />
    Copy the code
  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)}}}Copy the code
  8. Results the following


Flutter and Android interaction

Android brings up the Flutter page

There is already code to open the Flutter page in the code above, as follows:

startActivity(FlutterActivity.createDefaultIntent(this))
Copy the code

However, if you run the code, it will start very slowly. Here is 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(a): FlutterEngine {
        Create a Flutter engine
        val flutterEngine = FlutterEngine(this)
        // Specify a flutter page to jump to
        flutterEngine.navigationChannel.setInitialRoute("main")
        flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
        // Make a cache here, you can execute it at the appropriate time, such as in the app, before the jump to perform the preload
        val flutterEngineCache = FlutterEngineCache.getInstance();
        flutterEngineCache.put("default_engine_id", flutterEngine)
        // The above code is usually called before a jump to make the jump tree faster
        return flutterEngine
    }
    
    override fun onDestroy(a) {
       super.onDestroy()
       flutterEngine.destroy()
    }
}
Copy the code

The Flutter code 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 the code

The effect is as follows:

You can see that the jump speed is significantly faster.

It should be noted that the page will not change after modifying the code in Fluuter_model and running Android again. In android project, the code of flutter exists in the form of an AAR package, so after the flutter code is updated, To create a new AAR package, run the flutter build aar command again.

This doesn’t mean you should do this every time, of course; in normal development, just run the Flutter_module directly. Run the command when it needs to be closed.

When using a cached FlutterEngine, the FlutterEngine outlives any FlutterActivity or FlutterFragment that displays it. Please keep in mind that the Dart code immediately after you preheat FlutterEngine began to perform, and continue after your FlutterActivity/FlutterFragment destroyed. To stop execution and clear the resource, get the FlutterEngine from FlutterEngineCache and destroy the FlutterEngine using FlutterEngineCache ().

Finally, if you want to test performance, use the Release version

Jump to Flutter with ginseng

If you need to carry parameters during the jump, you just need to splice parameters after route, as shown below:

flutterEngine.navigationChannel.setInitialRoute("main? {\"name\":\"345\"}")
Copy the code

Is routing and parameters used here? The parameters are passed in JSON format.

The route + parameter is retrieved on the Flutter side using window.defaultRouteName. We just need to parse it:

String url = window.defaultRouteName;
/ / the name of the route
String route =
    url.indexOf('? ') = =- 1 ? url : url.substring(0, url.indexOf('? '));
// Parameter Json string
String paramsJson =
    url.indexOf('? ') = =- 1 ? '{}' : url.substring(url.indexOf('? ') + 1);
// Parse the parameters
Map<String.dynamic> params = json.decode(paramsJson);
Copy the code

Use the code above to get the jump parameters

Start FlutterActivity transparently
startActivity(
    FlutterActivity.withCachedEngine("default_engine_id")
        .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
        .build(this))Copy the code
Start FlutterActivity in a translucent manner

1. You need a theme property for translucency

<style name="MyTheme" parent="@style/MyParentTheme">
  <item name="android:windowIsTranslucent">true</item>
</style>
Copy the code

2. Apply the theme to FlutterActivity

<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 the code

This allows FlutterActivity to support translucency

Android embedded FlutterFragment

To display a FlutterFragment on an Android page, do the following:

class MainActivity : AppCompatActivity() {
    // Define a tag string to represent the FragmentManager of the FlutterFragment activity within 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 {
            // Try to find the existing FlutterFragment in case this is not the first time onCreate() has been run
            flutterFragment =
                supportFragmentManager.findFragmentByTag(tagFlutterFragment) as 			FlutterFragment?
            / / create FlutterFragment
            if (flutterFragment == null) flutterFragment =
                FlutterFragment
                    .withCachedEngine("default_engine_id")
                    .build()
            / / load FlutterFragmentsupportFragmentManager .beginTransaction() .add(R.id.layout, flutterFragment!! , tagFlutterFragment) .commit() } }private fun initFlutterEngine(a): FlutterEngine {
        Create a Flutter engine
        val flutterEngine = FlutterEngine(this)
        // Specify a flutter page to jump to
        flutterEngine.navigationChannel.setInitialRoute("main")
        flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
        // Make a cache here, you can execute it at the appropriate time, such as in the app, before the jump to perform the preload
        val flutterEngineCache = FlutterEngineCache.getInstance();
        flutterEngineCache.put("default_engine_id", flutterEngine)
        // The above code is usually called before a jump to make the jump tree faster
        return flutterEngine
    }

    override fun onPostResume(a) {
        super.onPostResume() flutterFragment? .onPostResume() }override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent) flutterFragment? .onNewIntent(intent) }override fun onBackPressed(a) {
        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(a) {
        super.onUserLeaveHint() flutterFragment? .onUserLeaveHint() }override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level) flutterFragment? .onTrimMemory(level) }override fun onDestroy(a) {
        super.onDestroy()
        flutterEngine.destroy()
    }
}
Copy the code

The above code opens FlutterFragmetn directly by initializing the engine, which has the benefit of loading more chunks.

It is important to note that in order to achieve all the expected behavior of a Flutter, these signals must be forwarded to the FlutterFragment, which is why there are so many new methods above.

Runs FlutterFragment from the specified entry point

Similar to different initial routes, different Flutterfragments may want to perform different Dart entry points. In a typical Flutter application, there is only one Dart entry point :main(), but you can define other entry points.

FlutterFragment supports the specification of Dart entry points required to perform a given Flutter experience. To specify an entry point, build the FlutterFragment as follows:

FlutterFragment.withNewEngine()
    .dartEntrypoint("newMain")
    .build()
Copy the code

FlutterFragment starts an entry point named newMian.

The configuration of the flutter terminal is as follows:

void main() => runApp(MyApp(window.defaultRouteName));

void newMain() => runApp(NewMainApp());
Copy the code

It is important to note that this must be configured in the main.dart file.

The Dart entry point attribute is invalid when the FlutterFragment uses the cache, so the cache cannot be used after the entry is specified.

Controls the rendering mode of the FlutterFragment

Flutter can use SufaceView to render its content, or TextureView.

FlutterFragment uses the SurfaceView by default. It is significantly newer than TextureView, but SufaceView cannot be crossed in the Android View hierarchy, SurfaceView must be either the bottom View or the top View.

Additionally, in versions prior to Android N, SurfaceViews cannot use animations because their layout rendering is different from the rest of the View hierarchy.

Then you need to use TextureView instead of SurfaceView. Select TextureView by building FlutterFragment with RenderMode, as shown below:

val flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .renderMode(FlutterView.RenderMode.texture)
    .build()
Copy the code
FlutterFragment with transparency

By default, FlutterFragment uses the SurfaceView to render opaque backgrounds. The background is black for any pixel that is not drawn by Flutter. For performance reasons, rendering with opaque backgrounds is the preferred rendering mode. Flutter rendering with transparency on Android can negatively impact performance. However, there are many designs that require transparent pixels to be displayed in the Flutter experience, and these pixels are displayed in the underlying Android UI. Therefore, Flutter supports translucency in the FlutterFragment

Both SurfaceView and TextureView support transparency. However, when the SurfaceView is instructed to render transparently, it positions itself on a higher Z-index than all other Android views, which means it appears on top of all other views. This is the SurfaceView limitation. If it is acceptable to render your Flutter experience on top of everything else, then the default RenderMode on the surface of FlutterFragment is the RenderMode you should use. However, if you want to display Android views above and below the Flutter experience, you must specify rendermode.texture. There are

The usage is as follows:

FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
    .transparencyMode(FlutterView.TransparencyMode.transparent)
    .build();
Copy the code
The relationship between FlutterFragment and its activities

Some apps choose to use Fragments as their entire Android screen. In these applications, it makes sense to use fragments to control systems such as Chrome, Android’s status bar, navigation bar, and directions.

In other applications, fragments are used to represent only a portion of the UI. FlutterFragment can be used to implement the interior of a drawer, video player, or single card. In these cases, it is not appropriate for FlutterFragment to affect The Android operating system Chrome, because there are other UI fragments in the same Window.

FlutterFragment comes with a concept that helps distinguish between situations in which a FlutterFragment should be able to control its host Activity, and situations in which a FlutterFragment should only affect its own behavior. To prevent FlutterFragment from exposing its Activity to the Flutter plugin and from the system UI that controls the Flutter Activity, Please use FlutterFragment Builder of shouldAttachEngineToActivity () method, 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 the code

Flutter communicates with Android

Platform Channel is a tool for Flutter and native communication. It comes in three types:

  • BaseicMessageChannel: Used to transfer strings and semi-structured information. This function can be used by Flutter and platforms to exchange message data.
  • MethodChannelMethod Invocation: Used for direct method invocation of the Flutter and platform
  • EventChannelCommunication of user data streams, Flutter and platform-side event listening, cancellation, etc

Methodchannels are the most commonly used MethodChannel in daily development, and you can check out the other two online

Android calls the Flutter method
val methodChannel =
    MethodChannel(flutterEngine.dartExecutor, "com.example.AndroidWithFlutter/native")
Copy the code

The code above defines an MtthodChannel. The first parameter is an interface that communicates with the Flutter, and the second parameter is name, which is the name of the channel (this name needs to be the same as that defined for Flutter).

// Call the Flutter method
methodChannel.invokeMethod("flutterMethod"."Call Flutter parameters".object : MethodChannel.Result {
	override fun success(result: Any?). {
		Log.e("-- -- -- 345 -- -- -- >"."$result");
	}

	override fun error(errorCode: String? , errorMessage:String? , errorDetails:Any?). {
		Log.e("-- -- -- 345 -- -- -- >"."Failed to invoke Flutter");
	}

	override fun notImplemented(a){}})}Copy the code

The above code calls a Flutter method named flutterMethod, where the first parameter is the method name, the second parameter, and the callback is the result of the call and whether the call succeeded. Let’s look at how Flutter is defined:

final _channel = const MethodChannel("com.example.AndroidWithFlutter/native");
@override
void initState() {
  super.initState();
  ///Listen for calls on android
  _channel.setMethodCallHandler((call) async {
    switch (call.method) {
      case "flutterMethod":
        print("Parameters:${call.arguments}");
        break;
    }
    return "I am the return value of Flutter";
  });
}

Copy the code

The above code listens to the Android call and then determines which method it is based on the method name.

Note that Flutter can be called even if the page is not opened. This is because the flutterEngine has been cached. FlutterEngine directly executes dart code, so it can be called directly. But if the cache is not used when the page jumps. This time, although it shows that the call is successful, but the jump to the past is not the corresponding parameter, because there is no cache, not the same object, so no, here need to pay attention to.

Flutter calls the Android method

Flutter end code:

void _incrementCounter() {
  // Call the AndroidMethod method of Android
  var result = _channel.invokeMapMethod("AndroidMethod"."Call Android Parameters");
  result.then((value) => print('Android return value:$value'));
}
Copy the code

Android code:

methodChannel.setMethodCallHandler { call, result ->
    when (call.method) {
        "AndroidMethod" -> {
            result.success(mapOf("Android Return value" to I am a Android \ '\' ""))}else -> {
            result.success("I'm an Android, I can't find the corresponding method.")}}}Copy the code

Note that a flutter call to Android must return a map.

Flutter jumps to the Android page

A MethodChannel is used by a flutter to jump to an android page. When a flutter needs to jump, it calls android and executes the jump logic on the android side, as shown below:

Flutter end code:

Void _incrementCounter() {// Open the native page _channel.invokemapMethod ("jumpToNative"); }Copy the code

Android code:

// Listen for flutter calls to Android
methodChannel.setMethodCallHandler { call, result ->
    when (call.method) {
        "AndroidMethod" -> {
            result.success(mapOf("Android Return value" to I am a Android \ '\' ""))}"jumpToNative"- > {// Go to the login page
            startActivity(Intent(this, LoginActivity::class.java))
        }
        else -> {
            result.success("I'm an Android, I can't find the corresponding method.")}}}Copy the code

The effect picture is as follows:

Page return parameter implementation

This method is implemented in much the same way as above, using a MethodChannel that calls the corresponding parameter when the page returns.

Memory usage

We made a memory observation of flutter after and without use in the project as follows:

No flutter Module is introduced:

Introduce a Flutter Module:

Start only one cache engine:

If you look at the image above, you can see that the memory usage before introduction was around 55Mb, but after initialization of the Fluuter Engine, memory was suddenly 181Mb. And this is initializing a single case.

Let’s see what happens when we initialize more than one:

    initFlutterEngine("init_one")
    initFlutterEngine("init_two")
    initFlutterEngine("init_three")

private fun initFlutterEngine(id: String): FlutterEngine {
    Create a Flutter engine
    val flutterEngine = FlutterEngine(this)
    // Specify a flutter page to jump to
    flutterEngine.navigationChannel.setInitialRoute("main")
    flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
    // Make a cache here, you can execute it at the appropriate time, such as in the app, before the jump to perform the preload
    val flutterEngineCache = FlutterEngineCache.getInstance();
    flutterEngineCache.put(id, flutterEngine)
    // The above code is usually called before a jump to make the jump tree faster
    return flutterEngine
}
Copy the code

The code looks like this, and here is the result:

As you can see, four caches were initialized and 355Mb was used. This is 174Mb more than the previous one, and each additional cache adds an average of 60Mb.

From the above verification, it can be concluded that the use of Flutter does increase memory greatly, but does not cause memory stress.

By comparing the increase of the cache engine, we found that each increase of a cache engine will increase about 60Mb.

To sum up:

Under normal circumstances, there is no problem when using, but it needs to be noted that when initializing the engine, you can initialize one. You cannot re-initialize the engine every time you open a page.

Project example

  • The Android client: github.com/LvKang-insi…
  • FlutterModule:github.com/LvKang-insi…

Recommended reading

  • This article learns about BuildContext
  • The principle and use of Key
  • Construction process analysis of Flutter three trees
  • Start, render, setState process
  • Flutter layout process

The resources

Docs. Flutter. Dev/development…

If this article is helpful to you, we are honored, if there are mistakes and questions in the article, welcome to put forward

My blog is synchronized to tencent cloud + community, invite everyone to come together: cloud.tencent.com/developer/s…