We know that native Android has two main ways to integrate Flutter. One is to create a Flutter Module and rely on it as a native Module. Another option is to package the Flutter Module as an AAR and then rely on the AAR package in the native project. The aar is officially recommended for access.

You can refer to my previous article on how to connect a Flutter AAR to a native Android project. Today I want to share with you the use of FlutterFragment.

First, Android native project

In Android native development, there are usually three ways to implement bottom Tab navigation, respectively:

  • RadioGroup + ViewPager + Fragment: Can preload adjacent fragments
  • FragmentTabHost + Fragment: Loads the selected Fragment
  • BottomNavigationView: Animated with a selection

Here, we use the BottomNavigationView for bottom Tab navigation. First, we create a new Android native project and then create three fragments. The activity_main.xml layout code is as follows:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">


    <FrameLayout
        android:id="@+id/fl_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:itemTextColor="@color/tab_text_color"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />

</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

The code introduces a bottom_nav_menu. XML layout as follows:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/nav_home"
        android:icon="@drawable/tab_home"
        android:title="@string/tab_home" />

    <item
        android:id="@+id/nav_car"
        android:icon="@drawable/tab_car"
        android:title="@string/tab_car" />

    <item
        android:id="@+id/nav_me"
        android:icon="@drawable/tab_mine"
        android:title="@string/tab_me" />
</menu>
Copy the code

Where, BottomNavigationView commonly used properties are as follows:

  • App :iteamBackground: Refers to the background color of the bottom navigation bar, the default is the theme color
  • App :menu: refers to the bottom menu (text and pictures are written in this, it is recommended to use vector pictures)
  • App :itemTextColor: Specifies the color of the text in the navigation bar
  • App :itemIconTint: Refers to the color of the picture in the navigation bar

Finally, implement Tab switch in mainactivity. Java, the code is as follows:

class MainActivity : AppCompatActivity() { private var fragments = mutableListOf<Fragment>() private var lastfragment = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initFragment() initNavigationSelectedListener() } private fun initFragment() { val homeFragment = HomeFragment() val carFragment = CarFragment() val mineFragment = MineFragment() fragments.add(homeFragment) fragments.add(carFragment) fragments.add(mineFragment) supportFragmentManager.beginTransaction() .replace(R.id.fl_container, homeFragment) .show(homeFragment) .commit() } private fun switchFragment(index: Int) { if (lastfragment ! . = index) {val transaction = supportFragmentManager beginTransaction () / / hide the last fragments transaction.hide(fragments[lastfragment]) if (! fragments[index].isAdded) { transaction.add(R.id.fl_container, fragments[index]) } transaction.show(fragments[index]).commitAllowingStateLoss() lastfragment = index } } private fun initNavigationSelectedListener() { findViewById<BottomNavigationView>(R.id.bottom_navigation).setOnNavigationItemSelectedListener { item -> when (item.itemId) { R.id.nav_home -> { switchFragment(0) return@setOnNavigationItemSelectedListener true } R.id.nav_car -> {  switchFragment(1) return@setOnNavigationItemSelectedListener true } R.id.nav_me -> { switchFragment(2) return@setOnNavigationItemSelectedListener true } } false } } }Copy the code

Introduce the Flutter Module

First, create a Flutter Module project. There are two ways to create a Flutter Module, either using Android Studio or directly using the command line. Create a Flutter Module using the command line as follows:

flutter create -t module flutter_module
Copy the code

Then, into the flutter_module, perform flutter build aar command to generate the aar package, without any error, in/flutter_module /. Android/flutter/build/outputs directory to generate the corresponding aar package, The diagram below.

Next, we copy the generated AAR package into the Android project liBS and open app/build.grade to add local dependencies.

repositories { flatDir { dirs 'libs' } } dependencies { ... Implementation fileTree(dir: 'libs', include: ['*.jar']) implementation(name: 'flutter_RELAese -1.0', ext: 'the aar') implementation 'IO. Flutter: flutter_embedding_debug: 1.0.0 - f0826da7ef2d301eb8f4ead91aaf026aa2b52881' implementation 'the IO. Flutter: armeabi_v7a_debug: 1.0.0 - f0826da7ef2d301eb8f4ead91aaf026aa2b52881' implementation 'the IO. Flutter: arm64_v8a_debug: 1.0.0 - f0826da7ef2d301eb8f4ead91aaf026aa2b52881' implementation 'the IO. Flutter: x86_64_debug: 1.0.0 - f0826da7ef2d301eb8f4ead91aaf026aa2b52881'}Copy the code

Build. Gradle > build. Gradle > build. Gradle > build. Gradle > build.

buildscript { repositories { ... Maven {url "http://download.flutter.io" / / flutter rely on}} dependencies {classpath 'com. Android. View the build: gradle: 4.0.0' }}Copy the code

Use the Flutter Module

By default, Android provides FlutterActivity, Fragment, and FlutterView views. In this example, we’ll look at using fragments.

First, we create a FlutterEngineGroup object, which can be used to manage multiple FlutterEngines, and multiple FlutterEngines can share resources. To reduce resource usage of FlutterEngine, MyApplication code is as follows:

class MyApplication : Application() { lateinit var engineGroup: FlutterEngineGroup Override fun onCreate() {super.oncreate () // Create the FlutterEngineGroup object engineGroup = FlutterEngineGroup(this) } }Copy the code

Next, create a FlutterEngineManager cache management class and create a static method flutterEngine in the FlutterEngineManager to cache the flutterEngine.

object FlutterEngineManager { fun flutterEngine(context: Context, engineId: String, entryPoint: String): FlutterEngine { // 1. From the cache access FlutterEngine var engine. = FlutterEngineCache getInstance () get (engineId) if (engine = = null) {/ / If there is no FlutterEngine in the cache // 1. The newly built FlutterEngine, Perform the entrance was entryPoint function of val app = context. The applicationContext as MyApplication val dartEntrypoint = DartExecutor.DartEntrypoint( FlutterInjector.instance().flutterLoader().findAppBundlePath(), entryPoint ) engine = app.engineGroup.createAndRunEngine(context, dartEntrypoint) // 2. Deposited in the cache FlutterEngineCache. GetInstance (). The put (engineId, engine)} return engine!! }}Copy the code

In the above code, we get the cached FlutterEngine from it, create a new one if not, and then cache it.

Next, we bind FlutterEngine and FlutterFragment. If no route is provided by default, the route home page of the Flutter Module is opened. To specify the home page of a Flutter Module, use the setInitialRoute() method.

Class HomeFragment: Fragment() {// 1. FlutterEngine private var engineId="home_fra" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 2. Get FlutterEngine object through FBFlutterEngineManager engine = FlutterEngineManager. FlutterEngine (requireActivity (), engineId, "main") // 3. Construct a FlutterFragment with FlutterEngine object val FlutterFragment = FlutterFragment.withCachedEngine(engineId).build<FlutterFragment>() // 4. Display FlutterFragment parentFragmentManager. BeginTransaction (). The replace (R.i d.h. ome_fl, flutterFragment).commit() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_home, container, false) } }Copy the code

Here we use cached FlutterEngine to save resources, because the Fragment of the Bottom Navigation Activity will be re-created and destroyed when switching back and forth, which consumes resources.

If you want to hide the BottomNavigationView in activity_main. XML after entering the secondary page and returning it, the code involved is as follows.

class MainActivity : AppCompatActivity() { ... Fun switchBottomView(show: Boolean) {val navView: BottomNavigationView = findViewById(R.id.nav_view) if (show) { navView.visibility = View.VISIBLE } else { navView.visibility = View.GONE } } }Copy the code

To interact with Flutter data, we can use MethodChannel, and then use setMethodCallHandler to call back Android data to Fluter, as shown below.

Class HomeFragment: Fragment() {// 1. FlutterEngine private var engineId="home_fra" private lateinit var channel: MethodChannel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initEngine() initChannel() } private fun initEngine() { // 2. Get FlutterEngine object through FBFlutterEngineManager engine = FlutterEngineManager. FlutterEngine (requireActivity (), engineId, "main") // 3. Construct a FlutterFragment with FlutterEngine object val FlutterFragment = FlutterFragment.withCachedEngine(engineId).build<FlutterFragment>() // 4. Display FlutterFragment parentFragmentManager. BeginTransaction (). The replace (R.i d.h. ome_fl, flutterFragment).commit() } private fun initChannel() { channel = MethodChannel(engine.dartExecutor.binaryMessenger, "tab_switch") channel.setMethodCallHandler { call, result -> when (call.method) { "showTab" -> { val activity = requireActivity() as MainActivity activity.switchBottomView(true) result.success(null) } "hideTab" -> { val activity = requireActivity() as MainActivity activity.switchBottomView(false) result.success(null) } else -> { result.notImplemented() } } } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_home, container, false) } }Copy the code

Then inside Flutter there is the invokeMethod method to inject.

class PluginManager { static const MethodChannel _channel = MethodChannel('tab_switch'); static Future<String> showTab(Map params) async { String resultStr = await _channel.invokeMethod('showTab', params); return resultStr; }}Copy the code

Currently, native mobile apps can integrate multiple Flutter Modules into their applications, which facilitates modular development of multiple businesses. In addition to FlutterActivity and Fragment, it is slightly complicated to use FlutterView in Android. It requires binding life cycle to use FlutterView, and developers need to manage the life cycle of FlutterView themselves.