Flutter混合開發之FlutterFragment使用

xiangzhihong發表於2022-01-09

我們知道,原生Android整合Flutter主要有兩種方式,一種是建立flutter module,然後以原生module那樣依賴;另一種方式是將flutter module打包成aar,然後在原生工程中依賴aar包,官方推薦aar的方式接入。

如何在原生Android工程中以aar的方式接入Flutter,大家可以參考我之前文章的介紹:原生Android工程接入Flutter aar。今天想給大家分享的是FlutterFragment的使用。

一、Android原生工程

在Android原生開發中,實現底部Tab導航通常有3種方式,分別是:

  • RadioGroup + ViewPager + Fragment:能夠預載入相鄰的Fragment
  • FragmentTabHost + Fragment:載入選中的Fragment
  • BottomNavigationView:有選中動畫效果

此處,我們使用BottomNavigationView來實現底部Tab導航。首先,我們新建一個Android原生工程,然後再新建三個 Fragment 。activity_main.xml佈局程式碼如下:

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

程式碼中引入了一個bottom_nav_menu.xml佈局,程式碼如下:

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

其中,BottomNavigationView常用的屬性如下:

  • app:iteamBackground:指的是底部導航欄的背景顏色,預設是主題的顏色
  • app:menu:指的是底部選單(文字和圖片都寫在這個裡面,推薦圖片使用向量圖)
  • app:itemTextColor:指的是導航欄文字的顏色
  • app:itemIconTint:指的是導航欄中圖片的顏色

最後,在MainActivity.java中實現Tab的切換,程式碼如下:

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()
            //隱藏上個Fragment
            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
        }
    }
}

二、引入Flutter Module

首先,建立一個Flutter Module工程。建立Flutter Module有兩種方式,一種是使用Android Studio進行生成,另一種是直接使用命令列。使用命令列建立flutter module的如下:

flutter create -t module flutter_module

然後,進入到flutter_module,執行flutter build aar命令生成aar包,如果沒有任何出錯,會在/flutter_module/.android/Flutter/build/outputs目錄下生成對應的aar包,如下圖。

在這裡插入圖片描述
接下來,我們把生成的aar包拷貝到Android工程的libs中,然後開啟app/build.grade新增本地依賴。

repositories {
    flatDir {
        dirs 'libs'
    }
}

dependencies {
    ...
    //新增本地依賴
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation(name: 'flutter_relaese-1.0', ext: 'aar')
    implementation 'io.flutter:flutter_embedding_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
    implementation 'io.flutter:armeabi_v7a_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
    implementation 'io.flutter:arm64_v8a_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
    implementation 'io.flutter:x86_64_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
}

然後在外層的build.gradle中申明為本地依賴,程式碼如下:

buildscript {
repositories {
   ... 
    maven {
        url "http://download.flutter.io"        //flutter依賴
    }
  }
  
dependencies {
    classpath 'com.android.tools.build:gradle:4.0.0'
  }
}

三、使用Flutter Module

預設情況下,Android提供了FlutterActivity、Fragment和FlutterView檢視,本例子我們講的是Fragment的使用。

首先,我們建立一個 FlutterEngineGroup 物件,FlutterEngineGroup 可以用來管理多個 FlutterEngine 物件,而多個 FlutterEngine 是可以共享資源的,目的是減少 FlutterEngine 的資源佔用,MyApplication的程式碼如下:

class MyApplication : Application() {

    lateinit var engineGroup: FlutterEngineGroup

    override fun onCreate() {
        super.onCreate()
        // 建立FlutterEngineGroup物件
        engineGroup = FlutterEngineGroup(this)
    }
}

接著,建立一個 FlutterEngineManager 快取管理類,在 FlutterEngineManager 中建立一個靜態方法 flutterEngine,用來快取FlutterEngine。

object FlutterEngineManager {

    fun flutterEngine(context: Context, engineId: String, entryPoint: String): FlutterEngine {
        // 1. 從快取中獲取FlutterEngine
        var engine = FlutterEngineCache.getInstance().get(engineId)
        if (engine == null) {
            // 如果快取中沒有FlutterEngine
            // 1. 新建FlutterEngine,執行的入口函式是entryPoint
            val app = context.applicationContext as MyApplication
            val dartEntrypoint = DartExecutor.DartEntrypoint(
                FlutterInjector.instance().flutterLoader().findAppBundlePath(), entryPoint
            )
            engine = app.engineGroup.createAndRunEngine(context, dartEntrypoint)
            // 2. 存入快取
            FlutterEngineCache.getInstance().put(engineId, engine)
        }
        return engine!!
    }
    
}

在上面的程式碼中,我們會先從中獲取快取的 FlutterEngine ,如果沒有則新建一個 FlutterEngine ,然後再快取起來。

接下來,我們將 FlutterEngine 和 FlutterFragment 進行繫結,如果預設沒有提供路由,那麼開啟的是flutter module的路由首頁。如果要指定flutter module的首頁,可以使用setInitialRoute()方法。

class HomeFragment : Fragment() {

    // 1. FlutterEngine物件
    private lateinit var engine: FlutterEngine
    private var engineId="home_fra"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 2. 通過FBFlutterEngineManager獲取FlutterEngine物件
        engine = FlutterEngineManager.flutterEngine(requireActivity(), engineId, "main")
        // 3. 用FlutterEngine物件構建出一個FlutterFragment
        val flutterFragment = FlutterFragment.withCachedEngine(engineId).build<FlutterFragment>()
        // 4. 顯示FlutterFragment
        parentFragmentManager.beginTransaction().replace(R.id.home_fl, flutterFragment).commit()
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_home, container, false)
    }
}

我們這裡使用快取的 FlutterEngine 更能節省資源,因為 Bottom Navigation Activity 的 Fragment 來回切換的時候, Fragment 是會重新新建和銷燬,比較消耗資源。

如果我們在進入將二級頁面時候,返回的時候,還需要將 activity_main.xml 中的 BottomNavigationView 隱藏,涉及的程式碼如下。

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

}

如果要和Flutter進行資料互動,那麼我們可以使用MethodChannel,然後使用setMethodCallHandler即可將Android資料回撥給Fluter,程式碼如下。

class HomeFragment : Fragment() {

    // 1. FlutterEngine物件
    private lateinit var engine: 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. 通過FBFlutterEngineManager獲取FlutterEngine物件
        engine = FlutterEngineManager.flutterEngine(requireActivity(), engineId, "main")
        // 3. 用FlutterEngine物件構建出一個FlutterFragment
        val flutterFragment = FlutterFragment.withCachedEngine(engineId).build<FlutterFragment>()
        // 4. 顯示FlutterFragment
        parentFragmentManager.beginTransaction().replace(R.id.home_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)
    }

}

接著在Flutter裡面是有invokeMethod方法注入即可。

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;
  }

}

目前原生移動APP可以在應用整合多個 Flutter Module ,這樣就方便我們進行多業務的模組化開發了。除了FlutterActivity、Fragment,在Android中可以使用FlutterView 會稍微複雜點,應使用個 FlutterView 需要繫結生命週期,需要開發者自己去管理FlutterView生命週期。

相關文章