将 Flutter 集成到 Android 项目
目录
将 Flutter 集成到 Android 项目
- 常见问题:FAQ
- 将 Flutter module 集成到 Android 项目
- 多个 Flutter 实例:multiple flutters
- 平台通道:platform channels
- 向应用添加闪屏页
- 在混合应用中管理 plugin 和依赖
前提条件
- Android 项目必须适配了 AndroidX
- Android 项目必须启用了
Java 8
- Flutter 项目必须是以
module
类型(而非默认的app
类型)创建的 - AOT 模式下仅支持
x86_64
,armeabi-v7a
和arm64-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
应用,可以打包apk
和aar
;而 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 ina 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 中的初始路由是不会生效的。如果想要在不同的Activity
和Fragment
之间使用同一个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 AndroidView hierarchy
, ASurfaceView
must either be the bottommost View in the hierarchy, or the topmost View in the hierarchy.- Before Android N,
SurfaceView
s can’t beanimated/transformed
, because their layout and rendering aren’t synchronized with the rest of theView hierarchy
.
默认情况下,FlutterFragment
使用 SurfaceView
渲染且背景不透明(opaque
)。任何未经 Flutter 绘制的像素在背景中都是黑色的。出于性能方面的考虑,应优先选择使用不透明的背景进行渲染。渲染透明(transparency
)的 Flutter 界面在 Android 平台上会产生性能方面的负面影响。但是许多设计都需要 Flutter 界面中包含透明的像素以显示底层的 Android UI。基于这个原因,Flutter 也支持 FlutterFragment
半透明(translucency
)。
SurfaceView
和TextureView
都支持透明(transparency
)。但是当SurfaceView
以透明模式渲染时,它的 Z 轴高度会超过其它所有 Android View,这意味着SurfaceView
会展示在其它所有 View 之上。这是SurfaceView
自身的限制。
添加 FlutterView
通过 FlutterView 进行集成是一种高级用法,requires manually creating custom, application specific bindings。
2022-5-15
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/16274981.html