End

将 Flutter 集成到 Android 项目

本文地址


目录

将 Flutter 集成到 Android 项目

前提条件

  • Android 项目必须适配了 AndroidX
  • Android 项目必须启用了 Java 8
  • Flutter 项目必须是以 module 类型(而非默认的 app 类型)创建的
  • AOT 模式下仅支持 x86_64armeabi-v7aarm64-v8a 架构
  • 其他小要求
    • 不支持同时打包多个 Flutter 模块
    • 建议集成 Kotlin,Module 项目只能使用 Kotlin 语言 -- Android 项目不使用 Kotlin 也是可以的
    • 新版本的 Android Studio + Flutter 中,已经没办法 New Flutter Module 了,只能手动集成
    • 基于 Android add-to-app 的插件必须迁移至基于 FlutterPlugin 的新版 Android Plugin API
    • 不支持 FlutterPlugin 的插件如果在 add-to-app 进行一些不合理的假设,可能会出现意外行为

Flutter module 的特点

module 项目和 app 项目的主要区别:

  • module 中的平台目录名都是以 . 开头的隐藏目录,例如 .android .ios;而 app 中是不以 . 开头的正常目录
  • module 中的平台目录在 build 后会自动生成,会被 git 忽略,可以随意删除;而 app 中是项目构建所必须的目录
  • module 仅支持构建 Android/iOS 应用,可以打包 apkaar;而 app 可以用于所有平台,但不能打包 aar

关于 .android 目录

  • 由于 .android 目录是自动生成的,因此不需要对它的代码进行版本控制
  • 在构建模块之前,可以先在模块根目录中运行 flutter pub get 重新生成 .android 目录
  • .android/ 目录中所做的任何更改,都不会显示在使用该模块的现有 Android 项目中

创建 Flutter module

建议直接使用 AS 创建,File -> New -> New Flutter Project -> Project Type 要勾选 module

flutter create -t module --org com.bqt my_flutter

-t, --template          Specify the type of project to create
-a, --android-language  开发使用的语言, either Java or Kotlin (default, recommended)
--org                   This string is used in Java package names
--project-name          This must be a valid dart package name
--platforms             本项目支持的平台,仅 app/plugin 时生效。可能需要先在 enable 某一平台
  • app:(default) Generate a Flutter application.
  • module:Generate a project to add a Flutter module to an existing Android or iOS application.
  • package:Generate a shareable Flutter project containing modular Dart code.
  • plugin:Generate a shareable Flutter project containing an API in Dart code with a platform-specific implementation through method channels for Android, iOS, Linux, macOS, Windows, web, or any combination of these.
  • plugin_ffi:Generate a shareable Flutter project containing an API in Dart code with a platform-specific implementation through dart:ffi for Android, iOS, Linux, macOS, Windows, or any combination of these.
  • skeleton:Generate a List View / Detail View Flutter application that follows community best practices.

依赖 Flutter module

依赖 Flutter module 的 aar

  • 可将 Flutter 库打包成由 aar 和 pom artifacts 组成的 本地 Maven 存储库
  • 宿主应用不需要安装 Flutter SDK,不需要依赖除 aar 之外的其他任何 Flutter 资源
  • 可通过 flutter build aar 命令,或在 AS 中通过 Build -> Flutter -> Build AAR 打包 AAR
// 在宿主项目的 build.gradle(旧版本语法) 或 settings.gradle(新版本语法) 中配置 maven 仓库地址
repositories {
    String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"
    maven { url 'some/path/my_flutter/build/host/outputs/repo' }
    maven { url "$storageUrl/download.flutter.io" }
}
// 在宿主项目的 app/build.gradle 中引用 build 后生成 aar
dependencies {
    // 可以针对 debug/release/profile 单独引用 aar
    implementation 'com.example.flutter_module:flutter_release:1.0'
}

build aar 后生成的目录结构

build/host/outputs/repo
└── com
    └── example                                        # 对应 groupId,也是 package_name
        └── my_flutter                                 # 对应 groupId,也是 flutter_module
            ├── flutter_profile
            ├── flutter_debug
            ├── flutter_release                        # 对应 artifactId
            │   ├── 1.0                                # 对应 version
            │   │   ├── flutter_release-1.0.aar
            │   │   ├── flutter_release-1.0.aar.md5
            │   │   ├── flutter_release-1.0.aar.sha1
            │   │   ├── flutter_release-1.0.pom        # flutter 所依赖的其他资源【重要】
            │   │   ├── flutter_release-1.0.pom.md5
            │   │   └── flutter_release-1.0.pom.sha1
            │   ├── maven-metadata.xml
            │   ├── maven-metadata.xml.md5
            │   ├── maven-metadata.xml.sha1

依赖 Flutter module 的源码

// 在宿主项目的 gradle.properties 中定义
flutter_module_android_path=C\:/_dev/flutter/qt_flutter_module/.android
// 在宿主项目的 settings.gradle 中执行指定的脚本
include ':app'

setBinding(new Binding([gradle: this]))
evaluate(new File(flutter_module_android_path, 'include_flutter.groovy'))
// 在宿主项目的 app/build.gradle 中引用
dependencies {
    implementation project(':flutter')
}

添加 FlutterActivity

注册 FlutterActivity

必须在宿主清单文件中注册 FlutterActivity

<activity
    android:name = "io.flutter.embedding.android.FlutterActivity"
    android:configChanges = "orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
    android:hardwareAccelerated = "true"
    android:theme = "@style/Theme.AppCompat.Light"
    android:windowSoftInputMode = "adjustResize"/>

启动 FlutterActivity

val intent = FlutterActivity   // 最基础的打开一个 FlutterActivity 的案例
    .createDefaultIntent(this) // 默认 DartEntrypoint 是 main(),默认初始路由是【/】
val intent = FlutterActivity   // 打开一个自定义 Flutter 初始路由的 FlutterActivity
    .withNewEngine()           // 创建一个新的 FlutterEngine,这会有一个明显的初始化时间
    .dartEntrypointArgs(null)  // Pass arguments to Dart's entrypoint function
    .initialRoute("/my_route") // 自定义初始路由,默认初始路由是【/】
    .build(this)

预热 FlutterEngine

每一个 FlutterActivity 默认会创建它自己的 FlutterEngine,创建 FlutterEngine 会有一个明显的 warm-up 时间,想要最小化这个延迟时间,可以在启动 FlutterActivity 之前,初始化一个 FlutterEngine,然后使用这个已经 pre-warmed FlutterEngine

val intent: Intent = FlutterActivity
    .withCachedEngine(QtFlutterEngine.getEngineId(this)) // 使用预热且缓存的 FlutterEngine
    .build(this)
class QtFlutterEngine private constructor() {
    companion object {
        @Volatile
        private var INSTANCE: FlutterEngine? = null
        private const val ENGINE_ID: String = "my_engine_id"

        fun getEngineId(context: Context, initialRoute: String? = null) = ENGINE_ID.also { init(context, initialRoute) }

        fun init(context: Context, initialRoute: String? = null): FlutterEngine {
            val isInvalid: Boolean = File(".soFileDir", ".soFileName").exists()
            if (isInvalid) {
                Log.i("bqt", "move .so file")
                INSTANCE?.destroy()  // Cleans up all components within this and destroys the associated Dart Isolate
                INSTANCE = null // This FlutterEngine instance should be discarded after invoking this method
            }
            return INSTANCE ?: synchronized(this) { // 第一次判空
                // 如果 INSTANCE 不为空,则返回 INSTANCE,否则返回 FlutterEngine(context),并执行指定的逻辑
                INSTANCE ?: FlutterEngine(context).also { initFlutterEngine(it, initialRoute) }
            }
        }

        private fun initFlutterEngine(flutterEngine: FlutterEngine, initialRoute: String? = null) {
            INSTANCE = flutterEngine
            if (initialRoute != null) {
                flutterEngine.navigationChannel.setInitialRoute(initialRoute) // 为缓存的 FlutterEngine 配置自定义的初始路由
            }
            val dartEntrypoint = DartExecutor.DartEntrypoint.createDefault()
            flutterEngine.dartExecutor
                .executeDartEntrypoint(dartEntrypoint)  // Start executing Dart code to pre-warm the FlutterEngine
            // FlutterEngine 其实是保存在一个单例对象的 Map 中,可以根据自己的需求增删改查
            FlutterEngineCache.getInstance() // Static singleton cache that holds FlutterEngine instances identified by String
                .put(ENGINE_ID, flutterEngine) // 其他 API:get/remove/contains/clear
        }
    }
}

预热 FlutterEngine 注意事项

  • 要预热一个 FlutterEngine,你必须执行一个 DartEntrypoint,当 executeDartEntrypoint() 方法调用时,DartEntrypoint 方法就会开始执行。如果 DartEntrypoint 中调用了 runApp(),then your Flutter app behaves as if it were running in a window of zero size,直至 FlutterEngine attach 到一个 FA/FF/FV 中。

  • 切记,Dart 代码会在预热 FlutterEngine 时就开始执行,并且在 FA/FF/FV destroy 后继续运行。要停止代码运行和清理相关资源,可以调用 FlutterEngine.destroy() 方法。

  • 运行时的 performance 不是预热和缓存一个 FlutterEngine 的唯一原因。一个预热的 FlutterEngine 会独立于 FA/FF/FV 执行 Dart 代码,即允许一个 FlutterEngine 在任意时刻执行任意 Dart 代码。非 UI 的应用逻辑可以在 FlutterEngine 中执行,例如网络请求和数据缓存,以及在 Service 中或其他地方的后台行为。当使用 FlutterEngine 在后台执行任务时,确保满足 Android 对于后台执行的所有限制。

  • 使用缓存中的 FlutterEngine 时,FA/FF 没有初始路由的概念,因为被缓存的引擎理论上已经执行了 Dart 代码,这时配置初始路由已经太迟了。在 runApp() 执行之后修改 navigation channel 中的初始路由是不会生效的。如果想要在不同的 ActivityFragment 之间使用同一个 FlutterEngine,并且在其展示时切换不同的路由,开发者需要设置一个 method channel,来显式地通知他们的 Dart 代码切换 Navigator 路由。

  • 即使使用了预热的 FlutterEngine,第一次展示 Flutter 的内容仍然需要一些时间。为了更进一步提升用户体验,Flutter 支持在第一帧渲染完成之前展示闪屏页。关于如何展示闪屏页的详细说明,请参阅这篇 闪屏页指南

启动透明的 FlutterActivity

使用透明的主题

<style name = "MyTheme" parent = "@style/Theme.AppCompat.Light">
    <item name = "android:windowIsTranslucent">true</item>
</style>

启动透明的 FlutterActivity

val intent: Intent = FlutterActivity
    .withCachedEngine(QtFlutterEngine.getEngineId(this))
    .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
    .build(this)

确保你的 Flutter 内容也有一个透明的背景。如果你的 Flutter UI 绘制了一个特定的背景颜色,那么你的 FlutterActivity 依旧看起来会是有一个不透明的背景。

添加 FlutterFragment

向 Android 应用中添加 Flutter Fragment

基础功能演示

class FlutterFragmentActivity : FragmentActivity() {

    private var flutterFragment: FlutterFragment? = null

    companion object {
        private const val FRAGMENT_TAG = "flutter_fragment"
        private const val TYPE = "TYPE"

        fun launche(context: Context, type: Int = 0) {
            val intent = Intent(context, FlutterFragmentActivity::class.java)
            intent.putExtra(TYPE, type)
            context.startActivity(intent)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.test)

        val fragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as FlutterFragment?
        flutterFragment = fragment ?: getFlutterFragment().also {
            supportFragmentManager.beginTransaction()
                .add(R.id.fragment_container, it, FRAGMENT_TAG)
                .commit()
        }
    }

    private fun getFlutterFragment(): FlutterFragment = when (intent.getIntExtra(TYPE, 0)) {
        1 -> FlutterFragment
            .withNewEngine() // Create a FlutterFragment with a new FlutterEngine and a desired engine configuration
            .initialRoute("myInitialRoute/") // 允许指定一个初始路由
            .dartEntrypoint("main") // The name of the initial Dart method to invoke, defaults to "main"
            .transparencyMode(TransparencyMode.transparent) // 启动一个透明的 FlutterFragment
            .renderMode(RenderMode.surface) // 支持三种渲染模式:surface/texture/image
            .shouldAttachEngineToActivity(true) // 是否应该控制宿主 Activity
            .build()
        2 -> FlutterFragment
            .withCachedEngine(QtFlutterEngine.getEngineId(this)) // 使用预热的 FlutterEngine
            .build() // 当使用已预热的 FlutterEngine 构建 FlutterFragment 时,指定 initialRoute、dartEntrypoint 是无效的
        else -> FlutterFragment.createDefault() // 以 main() 为 Dart 入口函数,以 / 为初始路由,并使用新的 FlutterEngine
    }

    override fun onPostResume() = super.onPostResume().also { flutterFragment?.onPostResume() }
    override fun onNewIntent(intent: Intent) = super.onNewIntent(intent).also { flutterFragment?.onNewIntent(intent) }
    override fun onBackPressed() = super.onBackPressed().also { flutterFragment?.onBackPressed() }
    override fun onUserLeaveHint() = super.onUserLeaveHint().also { flutterFragment?.onUserLeaveHint() }
    override fun onTrimMemory(level: Int) = super.onTrimMemory(level).also { flutterFragment?.onTrimMemory(level) }
    override fun onRequestPermissionsResult(r: Int, p: Array<String?>, g: IntArray) =
        super.onRequestPermissionsResult(r, p, g).also { flutterFragment?.onRequestPermissionsResult(r, p, g) }
}

控制渲染模式

FlutterFragment 可以选择使用 SurfaceView 或者 TextureView 来渲染其内容。默认配置的 SurfaceView 在性能上明显好于 TextureView

然而:

  • SurfaceView can’t be interleaved in the middle of an Android View hierarchy, A SurfaceView must either be the bottommost View in the hierarchy, or the topmost View in the hierarchy.
  • Before Android N, SurfaceViews can’t be animated/transformed, because their layout and rendering aren’t synchronized with the rest of the View hierarchy.

默认情况下,FlutterFragment 使用 SurfaceView 渲染且背景不透明(opaque)。任何未经 Flutter 绘制的像素在背景中都是黑色的。出于性能方面的考虑,应优先选择使用不透明的背景进行渲染。渲染透明(transparency)的 Flutter 界面在 Android 平台上会产生性能方面的负面影响。但是许多设计都需要 Flutter 界面中包含透明的像素以显示底层的 Android UI。基于这个原因,Flutter 也支持 FlutterFragment 半透明(translucency)。

SurfaceViewTextureView 都支持透明(transparency)。但是当 SurfaceView 以透明模式渲染时,它的 Z 轴高度会超过其它所有 Android View,这意味着 SurfaceView 会展示在其它所有 View 之上。这是 SurfaceView 自身的限制。

添加 FlutterView

通过 FlutterView 进行集成是一种高级用法,requires manually creating custom, application specific bindings。

2022-5-15

posted @ 2022-05-15 22:12  白乾涛  阅读(1189)  评论(0编辑  收藏  举报