Flutter 实践心路历程

Flutter 是一个基于 Dart 的移动开发平台,旨在帮助开发者在 iOS 和 Android 两个平台上开发高质量的跨平台应用,并在去年12 月的 Flutter Live 2018 大会上正式发布 1.0 正式版。

最近我也对 Flutter 进行了一个初步的了解和实践,过程中踩了许多坑,查了许多资料,使得自己对 Flutter 有了更进一步的理解和思考。

在本文中,我将分享自己在 Flutter 实践中的那些心路历程,主要涉及 Flutter 和 Native 混合工程开发的一些理解,希望能对大家有所帮助。

flutter

# 关于工程依赖问题的理解

总会遇到的几种工程类型

首先,明确一下在 Flutter 项目中会常遇到的工程类型,大致分为四种:

1、Application 类型

即标准化的 Flutter 工程,包含 Dart 层、由 Android 及 iOS 组成的Native 层

2、Flutter Module 类型

这是官方为现有 Native 工程引入 Flutter 提供的一种解决方案,它是一个可被 Native 工程依赖的 Module,仅包含 Dart 层,但其实 Module 内部隐藏了 Native 子工程。

3、Plugin 类型

通常是被发布的 Flutter 三方包形式,注意既包含 Dart 层,也包含 Native 层

4、Package 类型

仅包含 Dart 层的工程

也许可以不太恰当地将 Plugin 比作 AAR 包,将 Package 比作 Jar 包

Flutter 工程如何被依赖的

关于 Flutter 工程被 Native 工程依赖的问题,大致分为 2 种需要被解决的依赖对象,下文依次分析。

Flutter 框架本身

在 Flutter 项目的 Android 子工程中,app 层的 build.gradle 中被添加了一些代码,主要是对版本号的设置和对 flutter.gradle 的 apply,如下:

1
2
3
4
5
6
def flutterRoot = localProperties.getProperty('flutter.sdk')	// 获取 Flutter SDK 的路径
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
...
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" // apply Flutter SDK 下的 flutter.gradle

flutter.gradle 中做了些事情,来看看:

1
2
3
4
5
6
7
8
9
10
11
12
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
}

apply plugin: FlutterPlugin // 应用 FlutterPlugin 插件
...

主要的工作都在 FlutterPlugin 中,首先是找到对应平台的 Jar 文件:

1
2
3
4
5
6
7
8
9
String flutterRootPath = resolveProperty(project, "flutter.sdk", System.env.FLUTTER_ROOT)
flutterRoot = project.file(flutterRootPath)
String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile(); // 找到 Flutter SDK 下的 flutter 脚本

...

Path baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine")
debugFlutterJar = baseEnginePath.resolve("android-${targetArch}").resolve("flutter.jar").toFile() // 找到对应平台的 flutter.jar

Flutter/bin/cache/artifacts/engine 目录如下:

Engine 目录

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
// 如果没有找到 jar 文件,则使用「 flutter precache 」命令生成
if (!debugFlutterJar.isFile()) {
project.exec {
executable flutterExecutable.absolutePath
args "--suppress-analytics"
args "precache"
}
if (!debugFlutterJar.isFile()) {
throw new GradleException("Unable to find flutter.jar in SDK: ${debugFlutterJar}")
}
}

// x86 的 jar 和 so 库
flutterX86Jar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/flutter-x86.jar")
Task flutterX86JarTask = project.tasks.create("${flutterBuildPrefix}X86Jar", Jar) {
destinationDir flutterX86Jar.parentFile
archiveName flutterX86Jar.name
from("${flutterRoot}/bin/cache/artifacts/engine/android-x86/libflutter.so") {
into "lib/x86"
}
from("${flutterRoot}/bin/cache/artifacts/engine/android-x64/libflutter.so") {
into "lib/x86_64"
}
}
// 遍历为每种 buildTypes 依赖
project.android.buildTypes.each { addFlutterJarApiDependency(project, it, flutterX86JarTask) }
project.android.buildTypes.whenObjectAdded { addFlutterJarApiDependency(project, it, flutterX86JarTask) }

// 添加构建 Flutter Dart 代码的 Task
project.extensions.create("flutter", FlutterExtension)
project.afterEvaluate this.&addFlutterTask

引入三方 Plugin 包中的 Native 子工程

在 Flutter 项目的 pubspec.yaml 文件中,可声明项目依赖三方 Plugin 包,当执行 flutter packages get 命令时,会从仓库下载对应的源码包(Flutter 中所有的依赖都是基于下载源码进行依赖的),并将每一个包以 key - value 的形式将名称和路径保存在工程目录的 .flutter-plugins 文件中:

1
hybrid_stack_manager=/xxx/xxx/Flutter/.pub-cache/hosted/pub.dartlang.org/hybrid_stack_manager-0.1.0/
path_provider=/Users/xxx/xxx/Flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-0.4.1/

来看 Gradle 中对 Plugin 包依赖的处理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 从 .flutter-plugins 文件中读取所有的三方依赖 Plugins
File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
Properties plugins = readPropertiesIfExist(pluginsFile)

// 遍历三方 Plugins 文件地址,并让工程依赖它
plugins.each { name, _ ->
def pluginProject = project.rootProject.findProject(":$name")
project.dependencies {
if (project.getConfigurations().findByName("implementation")) {
implementation pluginProject
} else {
compile pluginProject
}
}
...
// 让每一个三方库包的 Native 工程也依赖 flutter 的 jar 包
pluginProject.afterEvaluate this.&addFlutterJarCompileOnlyDependency
}

# 混合工程页面栈的问题

Native 中被熟知的栈管理

Android 中通过 startActivity 启动 Activity,iOS 中的 navigationController 启动 ViewController,各自也都实现了一套页面栈机制,并且有丰富的路由管理三方库,这些都是 Native 开发者所熟知的,这里不在赘述。

Flutter 项目中的页面路由

简单场景的用法

Flutter 中通过 Navigator 中心化的管理者来对页面栈进行管理,用法简洁、清晰。首先是对「路由名」和「控件」的对应关系进行声明:

1
2
3
4
5
6
7
8
9
new MaterialApp(
home: new Screen1(),
routes: <String, WidgetBuilder> {
'/screen1': (BuildContext context) => new Screen1(),
'/screen2' : (BuildContext context) => new Screen2(),
'/screen3' : (BuildContext context) => new Screen3(),
'/screen4' : (BuildContext context) => new Screen4()
},
)

接下来可以进行 push 操作进行新页面的加载:

1
2
3
4
5
6
new RaisedButton(
onPressed:(){
Navigator.of(context).pushNamed('/screen2'); // Screen2 入栈
},
child: new Text("Push to Screen 2"),
),

对于页面栈切换的相关方法调用,参数可以是路由名称,也可以是构造的 Route 实例,便于灵活管理和参数传递

页面出栈,Flutter 提供了 pop 方法,同时提供 maybePop 和 canPop 方法来处理到达栈底的情况:

1
Navigator.of(context).pop();

关于页面替换的场景

页面替换是什么意思呢?分为两种情况,依次介绍。

开启新页面并杀掉自身

这是一种很常见的应用场景,比如 Splash Activity 到 MainActivity,或者登录页登录成功后到 MainActivity,都属于这种场景。对于这种场景,在 Android 开发中,我们通常是先 startActivity,再 finish 掉当前的 Activity,这样展示的便是启动一个新 Activity 的动画,并随即展示新 Activity 的界面。

而在 Flutter 中,提供的则是 pushAndReplace 的方法:

1
Navigator.of(context).pushReplacementNamed('/screen4');

调用前后的栈结构如图所示:

栈结构

杀掉自身并回到旧页面

典型的应用场景为:在一个新页面修改配置信息后,按「保存」回到前一页面,比如商品列表的筛选界面,筛选信息保存后回到商品列表页面,此时会仅展示离开的动画。Android 中 Activity 栈不太易于实现该效果,通常方法是以 startActivityForResult 启动新页面,退出新页面时展示离开动画,同时回调前一页面的 onActivityResult 方法,在其中可对数据进行刷新。

而在 Flutter 中,可以用页面栈很好地实现该效果:

1
Navigator.popAndPushNamed(context, '/screen4');

这种方法会把当前的页面 pop 掉,仅展示离开动画,同时将新页面入栈。

再如移除栈顶页面场景

移除栈顶页面简单说就是 Android 中的 FLAG_ACTIVITY_CLEAR_TOP 标记。应用场景很多,比如:在商品页面选购商品后,会有购物车、订单确认、支付确认等许多个后续页面依次排列在栈中,而当用户支付完成时,会直接回到最初的商品页面,同时 clear 掉栈中其上的所有页面。

对于这种场景,Flutter 也提供了支持:

1
Navigator.popUntil(context, ModalRoute.withName('/screen2'));

第二个参数表示何时停止 pop 操作,此处含义为遇到路由为 /screen2 是停止 pop。同时,还提供可以先 pop 再 push 的方法,这才是最像 FLAG_ACTIVITY_CLEAR_TOP 的特性:

1
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', ModalRoute.withName('/screen1'));

现状与未来

Flutter 中原生的 Navigator 路由方式,不论是 API 的 push 和 pop 操作,还是内部维护的 history 路由队列,都可以说是简洁而不失优雅的设计,同时业界阿里也在基于原生,推进 Flutter 的 Annotation_Route 注解式路由方案。

然而,在当今各种组件化盛行的时代, Native 界早已充斥着各家大名鼎鼎的 Router 方案,它们无一不具备零耦合、参数注入、跨组件等强大特性,Flutter 的 Router 能否胜任这些场景确实还真的值得考量。

而如果 Flutter 这种路由模式得到了肯定,又会不会反过来带进 Native 端呢

混合页面栈如何管理

话说回来,单纯 Flutter 应用的路由问题,基于其强大的 SDK,无论怎么都好解决。而相比起来,现阶段 Flutter 和 Native 混合的页面栈才是最大的问题。

混合页面栈什么意思呢?指的是 Native 页面和 Flutter 页面相互夹杂,存在于用户视角的全局栈中,这种问题的复杂性源于两个点:

栈

1、Flutter 和 Native 两端各自实现了一套互不相容的栈机制,且 Native 端对栈的控制权几乎为零;

2、双端相互唤起和释放页面如何通信的问题。

解决思路

接下来,将分析该问题解决的思路以及实现的案例。

问题一相对复杂,先看问题二:如何解决 Flutter 页面和 Native 页面相互唤起和释放,首先明确的是,Flutter 并没有提供对 Native 页面操作的方法,Native 端也没有提供对 Flutter 页面操作的方法,所以不可直接调用,思路转变为:页面上,还是各自控制各自的页面,只需要相互发个消息告知你需要我怎么操作页面即可。

这样一来,问题变得更加容易,Flutter 提供了和 Native 端通信的框架(关于此通信框架后文会对其进行介绍),我们可以分别在 Flutter 端和 Native 端初始化时,提供页面的操作方法,同时注册对方的消息通道,收到对方的消息时,对自身页面进行操作,如图:

消息通道

在 Flutter 的官方教程中,仅提出 Flutter 端调用 Native 的 Method Channel 方法调用通信通道,但事实上,SDK 也提供了 Native 端调用 Flutter 端方法的 API,是一个双向通道

回过头来看问题一,两端的栈机制不一致,那么如何维护管理两个页面栈呢?又由谁来维护这个公共栈呢?

如果交给 Flutter 端维护,则需要 Flutter 端维护 Activity 或 Native 的视图唯一引用,而 Native 的页面在 Flutter 端并不存在对应实体,无法直接操作,这样一来该方案显然是件成本高昂的事情;

而如果交给 Native 端维护,情况会好很多,因为 Flutter 在 Native 端以 FlutterView 的实体类存在,可以对其进行管理操作。

Flutter Activity

在 Native 端,每一个 Flutter 页面都使用包含一个 FlutterView 实例的 Activity 表示,即 Activity 作为 FlutterView 的 Container,这样不论是 Flutter 页面还是 Native 页面,都表现为 Activity。

当启动 Native 的 Activity 时,我们直接通过 startActivity 启动即可,没什么问题;

当启动包含 Flutter 的 Activity 时,我们实例化并初始化 FlutterView,并在 onCreate 方法中注册一个 Method Channel,用于提供 Flutter 端页面路由路径名,当 FlutterView 渲染时,会执行 main 方法,在方法中,我们可以调用 Native 端注册好的 Method Channel 来获取路由名,然后执行跳转,如图:

流程

别急,还有一个问题

这样做确实解决了 Flutter 和 Native 混合栈的问题,也能很好地完成入栈出栈。可是,FlutterView 的实例化和初始化成本是非常高昂的,这样每启动一个包含 Flutter 的 Activity 都会极耗系统资源。

关于这个问题,大致有这几种方案可以解决:

1、Fragment 封装

Fragment 封装

2、Flutter 引擎单例化

Flutter 引擎单例化

3、连续 Flutter 页面在一个 Activity 展示

连续 Flutter

有没有做好的轮子

阿里闲鱼出了个 hybrid_stack_manager,运用上述方案 2 和 3 结合的方式

hybrid-stack-manangement

有坑吗?有一些

1、没有不行的事,不行就修改源码一把梭

hybrid_stack_manager 库中需要对 Flutter 的路由进行一些定制化的操作,会用到 Flutter SDK 中的一些私有字段。在 Java 中我们通常可以使用反射的方式来获取类的信息,在 Dart 中其实也是支持反射机制的,但可能因为一些不可描述的原因,为了 Flutter 的稳定性,Flutter 中禁用了 Dart 的反射机制。

Flutter 中没有反射机制,所以一些 Java 中很多有趣的东西在 Flutter 中都难以实现,比如 Gson 这类便捷式 JSON 序列化和反序列化工具

所以如果需要引入库会让修改 Flutter SDK 的源码后,再进行编译引入。

1
List<Route<dynamic>> get history => _history;

2、Cocoapods 的 Bug 让 iOS 编译失败

Cocoapods 是 iOS 的包管理工具,因为一些 Bug 会导致在编译 Flutter 三方包时,找不到 Flutter SDK 的相关依赖,可以通过开发者自行修改 Cocoapods 自动生成的脚本文件来解决该问题

或许 hybrid_stack_manager 还不太完善,也可能还有很多坑没有被踩到,但相信随着生态圈的健全,这些问题都会得到完美的解决。

# 谈谈 Flutter 与 Native 通信

方法调用

Flutter 中提供 MethodChannel 进行方法调用,其是一个双向的通信通道,提供 Flutter 调用 Native 的方法和 Native 调用 Flutter 的方法

MethodChannel

事件监听

EventChannel 是 Flutter 提供的事件流监听机制,类似 Android 中的 Handler

基本消息传递

对于字符串和半结构化的信息,可以使用 BasicMessageChannel 进行消息的传递

# 如何工程化开发

组件分离

组件分离

1、对于依赖 Native 的组件,单独由 Flutter Plugin 完成

2、对于 UI 等纯 Dart 组件,可以由 Dart Packages 完成

工程隔离实施思路

1、Flutter SDK 要单独拉一份

2、Flutter SDK 需要 wrap,像 Gradlew 一样,避免版本差异

# 还想深入了解?

想对本文中提到一些技术点更加深入了解,可继续查阅下列几篇文章:

一个以注解方式实现的路由映射解决方案

Flutter: Push, Pop, Push

How to manage page stack in flutter/native hybrid App?

深入理解 Flutter Platform Channel