添加 Flutter 到 Android 原生项目中去-实践篇

本文为 Android 原生集成 Flutter 的实践,侧重点在于 Android 如何集成flutter,以及 Android 原生和 flutter 之间如何进行通讯。对于具体界面的开发,不会有太多的提及。

如果想要了解基础部分界面开发的内容,推荐你看下 flutter实战这本书, 里面对 widget、网络请求、国际化以及一些原理做了较为详细的介绍。但是关于架构部分,也就是状态管理部分较少有提及到,所以这里我也翻译了官方文档的状态管理参考部分,并将其中的社区博客部分中 Redux 和 BloC 部分进行了翻译,其中 Bloc 部分较为完整,Redux 部分只是零星翻译了几篇,当然基础的 setState、InheritedWidget 以及 Scoped model 部分也有一定篇幅。相信你看完这些之后,一定会有所收获。

实现的效果

这是 Android 原生的主页面,是使用 ViewPager 和 Fragment 实现的。
而此页面是其中的一个页面,我们将用 flutter 将其实现 。

点击某项之后进入详情页,此页面同样为 flutter 实现。

实现方案

根据官方的wiki,向已有应用添加flutter,我们可以看到比较完整的步骤。

创建 flutter module

在某一个目录下,执行 flutter create -t module my_flutter 创建 flutter module 项目。或者对于 熟悉 Android Stduio 的开发者,可以安装 flutter 开发插件,然后可以在 Android Studio 上实现以下操作。

我们选择的是 Flutter Module ,从而能以组件的形式添加到已有的 Android 或 ios 项目中。

实例化 FlutterView

Flutter 以 FlutterView 的形式集成在 Android 项目中,
并且为了不再承受 Android 的生命周期之痛,在 FlutterView 初始化的时候引入了 Lifecycle,如果想了解更多关于Lifecycle的内容,可以看这篇官方文档的翻译,使用可感知生命周期的组件处理生命周期

实例化 FlutterView 代码如下:

1
FlutterView flutterView = Flutter.createView(getActivity(), getLifecycle(), "route1");

而这个 FlutterView 其实就是个 SurfaceView。不同的是 SurfaceView 的渲染能力则是由 Flutter 所提供的。

原生 与 Flutter 之间的通讯

原生和 FlutterView 之间不可能是孤立的,建立两者之间的通讯才能实现我们的业务。

  • EventChannel
    EventChannel 是 原生主动要 Flutter 发送事件,而 flutter 处于监听这个事件的状态,有点推送的意思。
  • MethodChannel
    MethodChannel 是 Flutter 主动调用原生,而同时原生返回点什么给 Flutter,有点拉取的意思。

在此例子中,存在的交互主要有四种:

  • 页面初始化的时候,传入参数(EventChannel 实现)
  • 切换园区的时候,要将当前园区 id 传入 flutter 传入参数(EventChannel 实现)
  • 当登录信息过期时,跳转回到登录页面 (MethodChannel 实现)
  • 以及flutter调起 Activity,查看详情页面,而这个详情页的内容也是flutterView (MethodChannel 实现)

集成方式

以 module 的方式集成到项目中

在原生 Android 项目中集成 flutter module 有这么几个步骤

  • 将 flutter_module 于 你原先的Andorid项目放在同一级目录下
  • 在 Android 项目的 project 下的setting.gradle 的 include ‘:app’下增加以下内容:
1
2
3
4
5
6
include ':app' // new
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile, // new
'park_flutter_module/.android/include_flutter.groovy'
))
  • 在 app module 下 的 build.gradle 增加以下内容:
1
2
3
dependencies {
implementation project(':flutter')
}

这样就成功把项目引入到了Andorid 项目中。

当然事情没有这么顺利,因为 FlutterView 用了 lifecycle的东西。这是属于 Android 架构组件的东西,但是如果你已经把你的项目迁移到AndroidX的话,你会发现这个东西所属的包名已经从 android.arch.lifecycle.Lifecycle变成了import androidx.lifecycle.Lifecycle

为了保证两者的统一,你需要将你的 module 项目同样迁移到 AndroidX。

这样一番处理之后,你就可以正常运行你的Android项目了。

注意事项:不要把 .android 以及 .ios 提交到 你的 git,并且我们可以看到生成的 module的 ignore 文件是忽略了它们的。因为,它们是自动生成的,就算你提交了,很容易就被马上覆盖掉。每当 flutter 依赖有所变动,然后重新编译的时候都会重新生成。因为重新生成的缘故,你可能还得迁移好多次到AndroidX。

此种方式适合开发的时候使用,但功能开发完成之后,你可能不想在你的原生项目中看到所引入的那么多 module,这时候就可以使用第二种方式。

以 aar 的形式集成在项目中

设置 flavor

如果你设置了 Flavors ,请保持所有 module 的 变种的一致性。

例如,你的项目里面根据地址位置,分出了hz和jh。你就需要在依赖 module 中加入空白的 flavor 设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
buildTypes {

release {
signingConfig signingConfigs.debug
}

debug{

}
}
flavorDimensions "location"

productFlavors {

hz {
dimension "location"
}

jh {
dimension "location"
}

}

否则你可能又会把你的 flutter_assets 搞丢了,因为都是一一匹配的。

aar 编译

毫无疑问将 flutter 项目和 android 项目混在一起开发是比较痛苦的。你让那些不想写 flutter 的原生开发者也去忍受你的各种设置,真的是分分钟让人掉头发。

我们目标很明确就是让 flutter 成为 android 社区的好公民,不要搞什么特殊化,不需要特别对待。

选中 .android 右键

让我们回到我们最熟悉的 Android module 项目去寻找丢失的 flutter asset。

app module 是 flutter module 的宿主,也就是说 app 是 application ,而 flutter 是 library。

让我们先编译个 apk 试试,但是一运行你会发现以下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FAILURE: Build failed with an exception.

* Where:
Settings file '/Users/jinyulei/Desktop/demo1/flutter_module/.android/settings.gradle' line: 6

* What went wrong:
A problem occurred evaluating settings 'android_generated'.
> /Users/jinyulei/Desktop/demo1/flutter_module/.android/app/include_flutter.groovy (/Users/jinyulei/Desktop/demo1/flutter_module/.android/app/include_flutter.groovy)

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 0s

吸睛一看,你会发现 include_flutter.groovy 处于的位置和它实际的位置有点对不上,简单处理来说,我们将 settings.gradle 中的 include_flutter.groovy 路径直接修改为绝对路径。

或者你也可以在 Terminal 输入:

1
./gradlew flutter:assemble

在这个过程之后,我们生成了跟之前的一样的 aar 文件,依然没有 flutter_assets。

所以说,这个执行步骤依然不对。经过摸索之后,正确的执行步骤是这样的:

1
2
./gradlew app:assemble
./gradlew flutter:assemble

先执行 app:assemble 把 assets 准备好,这时候再运行 flutter:assemble 就可以打出包含
assets 的包来。

就拿这个项目来说吧,因为使用了 cachedImage 来处理图片缓存所以引入了 path_provider 和 sqflite 插件,插件并不会一同打入 flutter aar 中,所以需要将插件的 aar 都生成一遍,如下:

1
2
3
./gradlew path_provider:assembleHz

./gradlew sqflite:assembleHz

有了 flutter 及其依赖插件的 aar,然后就可以把 flutter 添加到 Android 项目了,并且这时候 Android 对 flutter 是无感的,它只是把它当作几个普通的 aar 文件。

注意,以上所有命令都是在 .android 目录下执行的。并且顺序是比较重要的,app 必须在 flutter 之前运行,其余插件的执行顺序没有关系。

添加 aar

这个步骤跟集成普通的 aar 文件没有什么区别。

修改 project 的 build.gradle 的 repositories 下 添加 flatDir

1
2
3
4
5
6
7
8
9
allprojects {
repositories {
google()
jcenter()
flatDir{
dirs 'libs'
}
}
}

随后在依赖中 app module 中 添加 aar 依赖。

1
2
3
4
5
6
dependencies {
...
implementation(name:'flutter-hz-release',ext:'aar')
implementation(name:'path_provider-hz-release',ext:'aar')
implementation(name:'sqflite-hz-release',ext:'aar')
}

以 maven 远程依赖的形式集成在项目中

aar 能解决项目分离的问题,但是从包管理的角度来说,不是一个很理想的方案,这时候我们就需要 maven 仓库来为我们进行管理。

考虑了网络和便捷性之后,我们选用的是阿里云的 maven 私有库来对 aar 文件进行管理。

阿里云提供了较为完善的上传操作界面

上传完成之后,你就可以在 module 中添加这些远程依赖了。

由于使用的是私有库,你需要设置下账户密码,设置方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
allprojects {
repositories {
maven {
url 'https://maven.aliyun.com/repository/public'
}

maven {
credentials {
username 'WUQqjg'
password '******'
}
url 'https://repo.rdc.aliyun.com/repository/74748-release-6AuZx7/'
}
maven {
credentials {
username 'WUQqjg'
password '******'
}
url 'https://repo.rdc.aliyun.com/repository/74748-snapshot-8cOvd4/'
}
}
}

```

然后在 module dependencies 中添加远程依赖:

```gradle
dependencies {
...
hzImplementation 'com.greentownit:flutter_park:1.0.1'
hzImplementation 'com.greentownit:path_provider:1.0.0'
hzImplementation 'com.greentownit:sqflite:1.0.0'
}

注意:由于我们设置了 flavor,在添加依赖的时候,我们需要进行指明 variant。

小问题

当 viewpager 滑动的时候,你会发现侧边会出现黑线,这一定是 SurfaceView 的锅,让我们来管管它。

1
2
3
4
5
flutterView.setZOrderOnTop(true);

flutterView.getHolder().setFormat(PixelFormat.TRANSPARENT);

flutterView.setBackgroundColor(Color.parseColor("#00000000"));

在初始化完成后的 flutterView 之后加上这么三句,把 holder 设为透明就搞定了。

非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号