Flutter混合开发组件化架构

  • 背景
  • Flutter的四种工程类型
  • Flutter工程Pub依赖管理
  • FlutterModule集成到Native
  • Flutter与Native通信
  • Flutter组件化工程
  • 后序

一、背景

Flutter 在目前跨平台方案中有更好的平台一致性以及更优的体验。但对于本身已有成熟的业务代码的项目来说,更多的是采用混合栈的方式,在不变更原有 App 业务的基础上,将 Flutter 能力扩展为子模块进行接入和开发。这样并不影响原有的业务和原生能力,又可以结合业务需求进行技术选择。

二、Flutter 的四种工程类型

2.1. Flutter Application

标准的Flutter App工程,包含标准的Dart层与Native平台层

2.2. Flutter Module

Flutter组件工程,仅包含Dart层实现,Native平台层子工程为通过Flutter自动生成的隐藏工程

2.3. Flutter Plugin

Flutter平台插件工程,包含Dart层与Native平台层的实现

2.4. Flutter Package

Flutter纯Dart插件工程,仅包含Dart层的实现,往往定义一些公共Widget

三、Flutter工程Pub依赖管理

Flutter工程之间的依赖管理是通过Pub来管理的,依赖的产物是直接源码依赖,这种依赖方式和IOS中的Pod有点像,都可以进行依赖库版本号的区间限定与Git远程依赖等,其中具体声明依赖是在pubspec.yaml文件中,其中的依赖编写是基于YAML语法,YAML是一个专门用来编写文件配置的语言。

声明依赖后,通过运行flutter packages get命名,会从远程或本地拉取对应的依赖,同时会生成pubspec.lock文件,这个文件和IOS中的Podfile.lock极其相似,会在本地锁定当前依赖的库以及对应版本号,只有当执行flutter packages upgrade时,这时才会更新。

四、Flutter module 集成到 Native

上述说的如果我们要利用Flutter来开发我们现有Native工程中的一个模块或功能,肯定得不能改变Native的工程结构以及不影响现有的开发流程,那么,以何种方式进行混合开发呢?

4.1 Flutter混合开发模式

Flutter混合开发模式一般有两种方式:

  • Flutter App 我们可以直接忽略,因为这是一个开发全新的Flutter App工程。
  • 对于Flutter Module,官方提供的本地依赖便是使用Flutter Module依赖到Native App的,而对于Flutter工程来说,构建Flutter工程必须得有个main.dart主入口,恰好Flutter Module中也有主入口。

4.2 Flutter Module的创建方式

Flutter module创建方式一般有两种:

a、通过命令来创建

flutter create -t module –org com.vhall.module vhall_flutter_module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Creating project vhall_flutter_module...
vhall_flutter_module/test/widget_test.dart (created)
vhall_flutter_module/vhall_flutter_module.iml (created)
vhall_flutter_module/.gitignore (created)
vhall_flutter_module/.metadata (created)
vhall_flutter_module/pubspec.yaml (created)
vhall_flutter_module/README.md (created)
vhall_flutter_module/lib/main.dart (created)
vhall_flutter_module/vhall_flutter_module_android.iml (created)
vhall_flutter_module/analysis_options.yaml (created)
vhall_flutter_module/.idea/libraries/Dart_SDK.xml (created)
vhall_flutter_module/.idea/modules.xml (created)
vhall_flutter_module/.idea/workspace.xml (created)
Running "flutter pub get" in vhall_flutter_module... 1,226ms
Wrote 12 files.

All done!
Your module code is in vhall_flutter_module/lib/main.dart.

b、使用 As 创建 Flutter Module

在 As 中选择 File->New->New Flutter Project,选择 Flutter Module 创建 Flutter Module 子项目

4.3 添加Flutter的两种依赖方式

4.3.1 将Flutter添加到原生工程中, 有两种方式:

a、以aar的方式集成到现有Android项目中

创建好 Flutter Module 之后需要将其编译成 aar 的形式,可以通过如下命令进行 aar 的编译:

cd vhall_flutter_module flutter build aar

在 Android 中也可以通过 As 工具来编译 aar,选择 Build->Flutter->Build AAR 来进行 aar 的编译。

然后根据提示在主项目工程的 build.grade 文件中进行相关配置,参考如下:

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
35
36
37
38
39
40
41
42
43
44

💪 Building with sound null safety 💪

Running Gradle task 'assembleAarDebug'... 22.2s
✓ Built build/host/outputs/repo.
Running Gradle task 'assembleAarProfile'... 46.1s
✓ Built build/host/outputs/repo.
Running Gradle task 'assembleAarRelease'... 37.2s
✓ Built build/host/outputs/repo.

Consuming the Module
1. Open <host>/app/build.gradle
2. Ensure you have the repositories configured, otherwise add them:

String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"
repositories {
maven {
url '/Users/zhangmiao/Documents/project/hybrid/vhall_flutter_module/build/host/outputs/repo'
}
maven {
url "$storageUrl/download.flutter.io"
}
}

3. Make the host app depend on the Flutter module:

dependencies {
debugImplementation 'com.vhall.module.vhall_flutter_module:flutter_debug:1.0'
profileImplementation 'com.vhall.module.vhall_flutter_module:flutter_profile:1.0'
releaseImplementation 'com.vhall.module.vhall_flutter_module:flutter_release:1.0'
}


4. Add the `profile` build type:

android {
buildTypes {
profile {
initWith debug
}
}
}

To learn more, visit https://flutter.dev/go/build-aar

优点:

  • 依赖一个包含 Flutter 产物的 aar 包,这个的好处就是其他不开发 flutter 的同学可以不用配置 flutter 环境,它和其他模块包无异

b、以 Flutet module 的方式集成到现有 Android 项目中

在 setting.gradle 文件中配置 flutter module 如下:

1
2
3
4
5
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir,
'../vhall_flutter_module/.android/include_flutter.groovy'
))

在 app 项目的 build.gradle 依赖 flutter module 模块

1
2
3
dependencies {
implementation project(':flutter')
}

缺点:

  • 需要 flutter 环境,并且各个开发人员环境不一致,导致集成因为版本不一致报各种错误

4.3.2本地依赖的原理

Android

在Android中本地依赖方式为:

  • settings.gradle中注入include_flutter.groovy脚本
  • 在需要依赖的app中build.gradle添加project(‘:flutter’)依赖

对于Android的本地依赖,主要是由include_flutter.groovyflutter.gradle这两个脚本负责Flutter的本地依赖和产物构建。

ainclude_flutter.groovy

settings.gradle中注入时,分别绑定了当前执行Gradle的上下文环境与执行include_flutter.groovy脚本,该脚本只做了下面三件事:

  • include FlutterModule中的.android/Flutter工程
  • include FlutterModule中的.flutter-plugins文件中包含的Flutter工程路径下的android module
  • 配置所有工程的build.gradle配置执行阶段都依赖于:flutter工程,也即它最先执行配置阶段

其中.flutter-plugins文件,是根据当前依赖自动生成的,里面包含了当前Flutter工程所依赖(直接依赖和传递依赖)的Flutter子工程与绝对路径的K-V关系,子工程可能是一个Flutter Plugin或者是一个Flutter Package。

bflutter.gradle

该脚本位于Flutter SDK中,内容看起来很长,其实主要做了下面三件事:

  • 选择符合对应架构的Flutter引擎(flutter.so)
  • 解析上述.flutter-plugins文件,把对应的android module添加到Native工程的依赖中(上述的include其实为这步做准备)
  • Hook mergeAssets/processResources Task,预先执行FlutterTask,调用flutter命令编译Dart层代码构建出flutter_assets产物,并拷贝到assets目录下

有了上述三步,则直接在Native工程中运行构建即可自动构建Flutter工程中的代码并自动拷贝产物到Native中

IOS

在IOS中本地依赖方式为:

  • 在Podfile中通过eval binding特性注入podhelper.rb脚本,在pod install/update时会执行它
  • 在IOS构建阶段Build Phases中注入构建时需要执行的xcode_backend.sh脚本

对于IOS的本地依赖,主要是由podhelper.rbxcode_backend.sh这两个脚本负责Flutter的Pod本地依赖和产物构建

apodhelper.rb

因Podfile是通过ruby语言写的,所以该脚本也是ruby脚本,该脚本在pod install/update时主要做了三件事:

  • Pod本地依赖Flutter引擎(Flutter.framework)与Flutter插件注册表(FlutterPluginRegistrant)
  • Pod本地源码依赖.flutter-plugins文件中包含的Flutter工程路径下的ios工程
  • 在pod install执行完后post_install中,获取当前target工程对象,导入Generated.xcconfig配置,这些配置都为环境变量配置,主要为构建阶段xcode_backend.sh脚本执行做准备

上述事情即可保证Flutter工程以及传递依赖的都通过pod本地依赖进Native工程了,接下来就是构建了

b、xcode_backend.sh

该Shell脚本位于Flutter SDK中,该脚本主要就做了两件事:

  • 调用flutter命令编译构建出产物(App.framework、flutter_assets)
  • 把产物(.framework、flutter_assets)拷贝到对应XCode构建产物中,对应产物目录为:*$HOME/Library/Developer/Xcode/DerivedData/${AppName}**

上述两个静态库*.framework是拷贝到${BUILT_PRODUCTS_DIR}”/“${PRODUCT_NAME}”.app/Frameworks”目录下

flutter_assets拷贝到${BUILT_PRODUCTS_DIR}”/“${PRODUCT_NAME}”.app”目录下

在XCode工程中,对应的是在${AppName}/Products/${AppName}.app

4.4 原生接入 flutter 页面

flutter 依赖提供了 FlutterActivity 来直接加载 flutter 页面,我们只需要在清单文件中配置该 Activity :

(通常我们会创建一个 Activity 继承 FlutterActivity)

1
2
3
4
5
6
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/Theme.Vhall_app"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"/>

三种打开flutter页面的方式:

1)普通跳转:

1
2
3
4
5
6
7
8
myButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity.createDefaultIntent(currentActivity)
);
}
});

2)设置路由的方式跳转:

1
2
3
4
5
6
7
8
9
10
11
myButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute("/my_route")
.build(currentActivity)
);
}
});

上述代码会在内部创建自己的 FlutterEngine 实例,每个 FlutterActivity 都创建自己的 FlutterEngine,这意味着启动一个标准的 FlutterActivity 会在界面可见时出现一短暂的延迟,可以选择使用预缓存的 FlutterEngine 来减小其延迟,实际上在内部会先检查是否存在预缓存的 FlutterEngine,如果存在则使用该 FlutterEngine,否则继续使用非预缓存的 FlutterEngine。

3)缓存 Flutter 引擎方式跳转:

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
public class MyApplication extends Application {
public FlutterEngine flutterEngine;

@Override
  public void onCreate() {
super.onCreate();
// Instantiate a FlutterEngine.
  flutterEngine = new FlutterEngine(this);

// Start executing Dart code to pre-warm the FlutterEngine.
  flutterEngine.getDartExecutor().executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault());

// Cache the FlutterEngine to be used by FlutterActivity.
  FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);
}
}

myButton.setOnClickListener(new View.OnClickListener() {
@Override
  public void onClick(View view) {
startActivity(
LoginFlutterActivity
  .withCachedEngine("my_engine_id")
.build(MainActivity.this)
);
  }
});

五、Flutter与Native通信

5.1Platform Channel

Platform Channel为Dart和平台之间提供了相互通信的机制,将FlutterAndroidiOS连接起来。

在移动H5开发中,webview自身提供的功能往往不够用,为了解决这个问题,引入了jsbridge,即webnative之间进行数据交互的一种方法,可以方便的将native的功能扩展给webview使用,从而可以快速开发。在Flutter中,也存在和jsbridge一样的用法,那就是Platform Channel,我们可以通过Platform Channel,将FlutterNative方便的连接在一起,架构图如下:

在Channel中

  1. client发送信息
  2. host接受信息并返回结果
  3. 而且消息和响应是以异步方式传递的
  4. Flutter和Natvie可以互为client和host,信息传递是双向的

5.2 三种不同类型的Platform Channel

Flutter定义了三种不同类型的Platform Channel用于Flutter与Host App平台进行通信,它们分别

  • BasicMessageChannel:用于数据传递,可以双向的请求数据。
  • MethodChannel:用于传递方法调用,即Flutter端可以调用Platform端的方法并通过Result接口回调结果数据。
  • EventChannel: 用于传递事件,即Flutter端监听Platform端的实时消息,一旦Platform端产生了数据,立即回调到Flutter端。

其构造方法都需指定一个通道标识、解编码器以及 BinaryMessenger,BinaryMessenger 是一个 Flutter 与平台的通信工具,用来传递二进制数据、设置对应的消息处理器等。

解编码器有两种分别是 MethodCodec 和 MessageCodec,前者对应方法后者对应消息,BasicMessageChannel 使用的是 MessageCodec,MethodChannel 和 EventChannel 使用的是 MethodCodec。

5.3 平台数据类型对照

Platform Channel 提供不同的消息解码机制,如 StandardMessageCodec 提供基本数据类型的解编码、JSONMessageCodec 支持 Json 的解编码等,在平台之间通信时都会自动转换,各平台数据类型对照如下:

六、Flutter组件化工程

6.1 背景

前面讲了Flutter和Native的混合开发模式,Flutter作为Native工程的一个Module存在,这样可以有效的将Flutter和Native进行物理隔离,但随着Flutter承载的业务越来越多,与Native交互的接口变的越来越多,带来了很多管理问题,因此我们迫切需要采用新的开发模式,即Flutter的组件化开发方案。

6.2 组件化的优势

采用组件化开发Flutter,将会有如下的优势:

  • 将功能模块化,相互独立,方便管理
  • 模块之间互不影响,耦合低,一些与业务无关的模块可以开源出来,供其他APP使用,提供代码的复用。
  • 采用组件化开发,开发时互不影响,可以提高开发效率。
  • 方便单元测试

6.3 组件化架构

组件划分,通过Flutter Module作为所有通过Flutter实现的模块或功能的聚合入口,通过它进行Flutter层到Native层的双向关联。而Flutter开发代码写在哪里呢?当然可以直接写在Flutter Module中,这没问题,而如果后续开发了多个模块、组件,我们的Dart代码总不可能全部写在Flutter Module中lib/吧,如果在lib/目录下再建立子目录进行模块区分,这不失为一种最简单的方式,不过这会带来一些问题,所有模块共用一个远程Git地址,首先在组件开发隔离上完全耦合了,其次各个模块组件没有单独的版本号或Tag,且后续模块组件的增多,带来更多的测试回归成本。

正确的组件化方式为一个组件有一个独立的远程Git地址管理,这样各个组件在发正式版时都有一个版本号和Tag,且在各个组件开发上完全隔离,后续组件的增多不影响其它组件,某个组件新增需求而不需回归其它组件,带来更低的测试成本。

前面提到Flutter Plugin可以有对应Dart层代码与平台层的实现,所以可以这样设计,一个组件对应一个Flutter Plugin,一个Flutter Plugin为一个完整的Flutter工程,有独立的Git地址,而这些组件之间不能互相依赖,保持零耦合,所以这些组件都在业务层,可以叫做业务组件,这些业务组件之间的通信和公共服务可以再划分一层基础层,可以叫做基础组件,所有业务组件依赖基础层,而Flutter Module作为聚合层依赖于所有Flutter组件,这些Flutter工程之间的依赖正是通过Pub依赖进行管理的。

所以,综合上述,整体的组件化架构可以设计为:

6.4业务组件与基础组件的定位

对于上面的基础组件比如还可以进行更细粒度的划分,不过不建议划分太多,对于与Native平台层的通信,每个业务组件对应一个Channel,当然内部还可以进行更细粒度的Channel进行划分,这个Channel主要是负责Native层服务的提供,让Flutter层消费。而对于Native层调用Flutter层的Api,应该尽可能少,需要调也只有出现一些值回调时。

因为Flutter的出现最本质的就是一次开发两端运行,而如果有太多这种依赖于平台层的实现,反而出现违背了,最后只是UI写了一份而已。对于平台层的实现也要尽量保持一个原则,即:

尽量让Native平台层成为服务层,让Flutter层成为消费层调用Native层的服务,即Dart调用Native的Api,这样当两端开发人员编写好一致基础的服务接口后,Flutter的开发人员即可平滑使用和开发。

七、后序

对于现有工程使用Flutter进行混合开发,坑点还是有的,比如性能、页面栈管理等方面,加上目前Flutter上一些基础库不成熟,对于项目内的重要页面以及动态化强度比较高的页面,目前还是不建议使用Flutter进行开发,如果要使用也须做好降级方案,相反可以使用稍微轻量级点的页面,且在设计时对于Flutter与Native层的通信,应该让Flutter作为消费层消费Native层提供的服务,Native端应做尽量少的改动等等。与纯原生开发或纯 Flutter 开发相比,混合开发由于需要打通原生和 Flutter 的数据和服务,需要有大量桥接实现,各个模块互相协作也需要考虑各种异常或降级的情况。

参考:
将 Flutter module 集成到 Android 项目 https://flutter.cn/docs/development/add-to-app/android/project-setup
将 Flutter module 集成到 iOS 项目 https://flutter.cn/docs/development/add-to-app/ios/project-setup
在 Android 应用中添加 Flutter 页面https://flutter.cn/docs/development/add-to-app/android/add-flutter-screen
在 iOS 应用中添加 Flutter 页面 https://flutter.cn/docs/development/add-to-app/ios/add-flutter-screen
Add-to-App Samples https://github.com/flutter/samples/blob/beface247a/add_to_app/README.md

坚持原创技术分享,您的支持将鼓励我继续创作!