一、简述
对于构建Flutter类型应用,因其开发语言Dart、虚拟机、构建工具与平时我们开发Native应用不同且平台虚拟机也不支持,所以需要Flutter SDK来支持,如构建Android应用需要Android SDK一样,下载Flutter SDK通常有两种方式:
在官网下载构建好的zip包,里面包含完整的Flutter基础Api,Dart VM,Dart SDK等
手动构建,Clone Flutter源码后,运行
flutter --packages get
或其它具有检测类型的命令如build
、doctor
,这时会自动构建和下载Dart SDK以及Flutter引擎产物
在团队多人协作开发下,这种依赖每个开发本地下载Flutter SDK的方式,不能保证Flutter SDK的版本一致性与自动化管理,在开发时如果Flutter SDK版本不一致,往往会出现Dart层Api兼容性或Flutter虚拟机不一致等问题,因为每个版本的Flutter都有各自对应的Flutter虚拟机,构建产物中会包含对应构建版本的虚拟机。Flutter工程的构建需要Flutter标准的工程结构目录和依赖于本地的Flutter环境,每个对应Flutter工程都有对应的Flutter SDK路径,Android在local.properties
中,IOS在Generated.xcconfig
中,这个路径会在Native工程本地依赖Flutter工程构建时读取,并从中获取引擎、资源和编译构建Flutter工程,而调用flutter
命令时构建Flutter工程则会获取当前flutter
命令所在的Flutter SDK路径,并从中获取引擎、资源和编译构建Flutter工程,所以flutter
命令构建环境与Flutter工程中平台子工程的环境变量一定得保持一致,且这个环境变量是随flutter
执行动态改变的,团队多人协作下这个得保证,在打包Flutter工程的正式版每个版本也应该有一个对应的Flutter构建版本,不管是本地打包还是在打包平台打包
我们知道Flutter应用的工程结构都与Native应用工程结构不一样,不一致地方主要是Native工程是作为Flutter工程子工程,外层通过Pub
进行依赖管理,这样通过依赖下来的Flutter Plugin/Package
代码即可与多平台共享,在打包时Native子工程只打包工程代码与Pub
所依赖库的平台代码,Flutter工程则通过flutter_tools
打包lib
目录下以及Pub
所依赖库的Dart代码。回到正题,因工程结构的差异,如果基于现有的Native工程想使用Flutter来开发其中一个功能模块,一般来说混合开发至少得保证如下特点:
对Native工程无侵入
对Native工程零耦合
不影响Native工程的开发流程与打包流程
易本地调试
显然改变工程结构的方案可以直接忽略,官方也提供了一种Flutter本地依赖到现有Native的方案,不过这种方案不加改变优化而直接依赖的话,则会直接影响了其它无Flutter环境的开发同学的开发,影响开发流程,且打包平台也不支持这种依赖方式的打包
再讲讲Flutter SDK,平时进行Flutter开发过程中,难免避免不了因Flutter SDK的Bug亦或是需要改Flutter SDK中平台链接的脚本代码导致直接改动或者定制Flutter SDK,这种方式虽然可以解决问题或定制化,不过极其不推荐,这种方式对后续Flutter SDK的平滑升级极不友好,且带来更多的后期维护成本
接下来,本文主要是介绍如何对上述问题解决与实现:
Flutter SDK版本一致性与自动化管理
无侵入Flutter SDK源码进行BugFix或定制化
Flutter混合开发组件化架构
Flutter混合开发工程化架构
二、Flutter四种工程类型
Flutter工程中,通常有以下几种工程类型,下面分别简单概述下:
1. Flutter Application
标准的Flutter App工程,包含标准的Dart层与Native平台层
2. Flutter Module
Flutter组件工程,仅包含Dart层实现,Native平台层子工程为通过Flutter自动生成的隐藏工程
3. Flutter Plugin
Flutter平台插件工程,包含Dart层与Native平台层的实现
4. Flutter Package
Flutter纯Dart插件工程,仅包含Dart层的实现,往往定义一些公共Widget
三、Flutter工程Pub依赖管理
Flutter工程之间的依赖管理是通过Pub
来管理的,依赖的产物是直接源码依赖,这种依赖方式和IOS中的Pod有点像,都可以进行依赖库版本号的区间限定与Git远程依赖等,其中具体声明依赖是在pubspec.yaml
文件中,其中的依赖编写是基于YAML
语法,YAML
是一个专门用来编写文件配置的语言,下面是一个通过Git地址远程依赖示例:
dependencies: uuid: git: url: git://github.com/Daegalus/dart-uuid.git ref: master
声明依赖后,通过运行flutter packages get
命名,会从远程或本地拉取对应的依赖,同时会生成pubspec.lock
文件,这个文件和IOS中的Podfile.lock
极其相似,会在本地锁定当前依赖的库以及对应版本号,只有当执行flutter packages upgrade
时,这时才会更新,同样pubspec.lock
文件也需要作为版本管理文件提交到Git中,而不应gitignore
1. Pub依赖冲突处理
对于Pub
和Pod
这种依赖管理工具对于发生冲突时处理冲突的能力与Android的Gradle
依赖管理相比差了一大截,所以当同一个库发生版本冲突时,只能我们自己手动进行处理,而且随着开发规模的扩大,肯定会出现传递依赖的库之间的冲突
Pub依赖冲突主要有两种:
当前依赖库的版本与当前的Dart SDK环境版本冲突
传递依赖时出现一个库版本不一致冲突
第一种会在flutter packages get
时报错并提示为何出现冲突且最低需要的版本是多少,如下:
The current Dart SDK version is 2.1.0-dev.5.0.flutter-a2eb050044. Because flutter_app depends on xml >=0.1.0 <3.0.1 which requires SDK version <2.0.0, version solving failed. pub get failed (1)
这个可以直接根据提示进行依赖库的版本升级解决
而第二种则比较复杂点,假如有A、B、C三个库,A和B都依赖C库,如果A的某个版本依赖的C和B版本依赖的C版本不一致,则会发生冲突,而如何解决这种冲突呢?有两种方式
1、首先把A和B库的版本都设为any
任意版本,如下:
dependencies: A: any B: any
此时再通过flutter packages get
时,则不会提示有版本冲突报错,因为Pub
已经自动选取了让C库版本一致的A、B库的版本号,此时打开同级目录下的pubspec.lock
文件,搜索A、B两个库,则会有对应无冲突的版本号,最后再把这两个版本号分别替换掉any
版本,这个版本冲突就解决了
2、通过版本覆盖进行解决
2. Pub依赖版本覆盖
在Pub
依赖管理中,既然支持传递依赖,同样也提供了一种版本覆盖的方式,意为强制指定一个版本,这和Android中Gradle
的force
有点相似,同样版本覆盖方式也可以用于解决冲突,如果知道某一个版本肯定不会冲突,则可直接通过版本覆盖方式解决:
dependency_overrides: A: 2.0.0
四、Flutter链接到Native工程原理
官方提供了一种本地依赖到现有的Native工程方式,具体可看官方wiki:Flutter本地依赖,这种方式太依赖于本地环境和侵入Native工程会影响其它开发同学,且打包平台不支持这种方式的打包,所以肯定得基于这种方式进行优化改造,这个后面再说,先说说Native两端本地依赖的原理
1. Android
在Android中本地依赖方式为:
在
settings.gradle
中注入include_flutter.groovy
脚本在需要依赖的module中
build.gradle
添加project(':flutter')
依赖
对于Android的本地依赖,主要是由include_flutter.groovy
和flutter.gradle
这两个脚本负责Flutter的本地依赖和产物构建
1. include_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,下面是.flutter-plugins
中的一段内容示例:
.flutter-plugins:
url_launcher=/Users/Sunzxyong/.pub-cache/hosted/pub.flutter-io.cn/url_launcher-4.0.2/
2. flutter.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中
2. IOS
在IOS中本地依赖方式为:
在Podfile中通过
eval binding
特性注入podhelper.rb
脚本,在pod install/update时会执行它在IOS构建阶段
Build Phases
中注入构建时需要执行的xcode_backend.sh
脚本
对于IOS的本地依赖,主要是由podhelper.rb
和xcode_backend.sh
这两个脚本负责Flutter的Pod本地依赖和产物构建
1. podhelper.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工程了,接下来就是构建了
2. 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
五、Flutter与Native通信
Flutter与Native通信有三种方式,这里只简单介绍下:
MethodChannel:方法调用
EventChannel:事件监听
BasicMessageChannel:消息传递
Flutter与Native通信都是双向通道,可以互相调用和消息传递
接下来是本文的重点内容,上述主要是普及下Flutter工程上比较重要的内容以及为下面要讲做准备,当然还有打包模式、构建流程等就不放这里了,后面可以单独开一篇讲
六、Flutter版本一致性与自动化管理
在团队多人协作开发模式下,Flutter SDK的版本一致性与自动化管理,这是个必须解决的问题,通过这个问题,我们回看Android中Gradle的版本管理模式:
Gradle的版本管理是通过包装器模式,每个Gradle项目都会对应一个Gradle构建版本,对应的Gradle版本在
gradle-wrapper.properties
配置文件中进行配置,如果执行构建时本地没有当前工程中对应的Gradle版本,则会自动下载所需的Gradle版本,而执行构建则是通过./gradlew
包装器模式进行执行,这样本地配置的全局Gradle环境与工程环境即可隔离开,对应的项目始终保持同一个Gradle版本的构建
这种包装器模式的版本管理方式,可与每台机器中全局配置的环境保持隔离,在团队多人协作下,也可保持同一个项目工程保持同一个构建版本
所以,我们沿用Gradle版本管理思想,在每个Flutter工程(包含上述说的四种工程)的根目录加入三个文件:
wrapper/flutter-wrapper.properties flutterw flutterw.bat
加入后的项目结构则多了三个文件,如下:
image
上述flutter-wrapper.properties
为当前工程Flutter SDK版本配置文件,内容为:
distributionUrl=https://github.com/flutter/flutterflutterVersion=1.0.0
当然有需要可以再增加一些配置,目前这两个配置已经足够了,指定了Flutter的远程地址以及版本号,如果Clone Github上项目比较慢,也可以改为私有维护的镜像地址
而flutterw
为一个Shell脚本,内部对版本管理主要做的事情为:
读取配置的版本号,校验Flutter SDK版本,不存在则触发下载
更新Android中
local.properties
和IOS中Generated.xcconfig
文件中Flutter SDK地址最后把命令行传来的参数链接到Flutter SDK中的flutter进行执行
之后构建Flutter工程则用flutterw
命令:
./flutterw build bundle
而不用本地全局配置的flutter
命令,避免每个开发同学版本不一致问题,且这种方式对于新加入Flutter开发的同学来说,完全不需要自己手动下载Flutter SDK,只需执行一下flutterw
任何命令,如./flutterw --version
,即可自动触发对应Flutter SDK的下载与安装,实现优雅的自动化管理,这种方式对打包平台来说也为支持Flutter工程的打包提供基础
七、Flutter混合开发组件化架构
上述说的如果我们要利用Flutter来开发我们现有Native工程中的一个模块或功能,肯定得不能改变Native的工程结构以及不影响现有的开发流程,那么,以何种方式进行混合开发呢?
前面说到Flutter的四种工程模型,Flutter App我们可以直接忽略,因为这是一个开发全新的Flutter App工程,对于Flutter Module,官方提供的本地依赖便是使用Flutter Module依赖到Native App的,而对于Flutter工程来说,构建Flutter工程
必须得有个main.dart
主入口,恰好Flutter Module中也有主入口
于是,我们进行组件划分,通过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
依赖进行管理的
所以,综合上述,整体的组件化架构可以设计为:
image
业务组件与基础组件的定位
对于上面的基础组件比如还可以进行更细粒度的划分,不过不建议划分太多,对于与Native平台层的通信,每个业务组件对应一个Channel
,当然内部还可以进行更细粒度的Channel
进行划分,这个Channel
主要是负责Native层服务的提供,让Flutter层消费。而对于Native层调用Flutter层的Api,应该尽可能少,需要调也只有出现一些值回调时
因为Flutter的出现最本质的就是一次开发两端运行,而如果有太多这种依赖于平台层的实现,反而出现违背了,最后只是UI写了一份而已。对于平台层的实现也要尽量保持一个原则,即:
尽量让Native平台层成为服务层,让Flutter层成为消费层调用Native层的服务,即Dart调用Native的Api,这样当两端开发人员编写好一致基础的服务接口后,Flutter的开发人员即可平滑使用和开发
而对于基础组件中的公共服务组件Dart Api层的设计,因为公共服务主要调用Native层的服务,在Flutter中提供公共的Dart Api,作为Native到Flutter的一个桥梁,对于Native的服务,会有很有多种,而对应Api的设计为一个dart文件对应一个种类的服务,整个公共服务组件提供一个统一个对外暴露的Dart,内部的细粒度的Dart实现通过export
导入,这种设计思想正是Flutter官方Api的设计,即统一对外暴露的Dart为common_service.dart
:
library common_service;export 'network_plugin.dart';export 'messager_plugin.dart'; ...
而上层业务组件调用Api只需要import一个dart即可,这样对上层业务组件开发人员是透明的,上层不需要了解有哪些Api可用:
import 'package:common_service/common_service.dart';
八、Flutter混合开发工程化架构
基本组件化的架构我们搭建好了,接下来是如何让Flutter混合开发进行完整的工程化管理,我们都知道,对于官方的本地依赖这种方式,我们不能直接用,因为这会直接影响Native工程、开发流程与打包流程,所以我们得基于官方这种依赖方式进行优化改造,于是我们衍生出两种Flutter链接到Native工程的方式:
本地依赖(源码依赖)
远程依赖(产物依赖)
为什么要有这两种方式,首先本地依赖对于打包平台不支持,现有打包平台的环境,只能支持标准的Gradle工程结构进行打包,且本地依赖对于无需开发Flutter相关业务的同学来说是灾难性的,所以便有了远程依赖,远程依赖直接依赖于打包好的Flutter产物,Android通过Gradle依赖,IOS通过Pod远程依赖,这样对其它业务开发同学来说是透明的,他们无需关心Flutter也不需要知道Flutter是否存在
对于这两种依赖模式的使用环境也各不一样
1. 本地依赖
本地依赖主要用于需要进行Flutter开发的同学,通过在对应Native工程中配置文件配置是否打开本地Flutter Module依赖,以及配置链接的本地Flutter Module地址,这样Native工程即可自动依赖到本地的Flutter工程,整个过程是无缝的,同时本地依赖是通过源码进行依赖的,也可以很方便的进行Debug调试
对于Android中配置文件为本地的local.properties
,IOS中为本地新建的local.xcconfig
,两个平台的配置属性保持一致:
FLUTTER_MODULE_LINK_ENABLE=trueFLUTTER_MODULE_LOCAL_LINK=/Users/Sunzxyong/FlutterProject/flutter_module
2. 远程依赖
远程依赖是把Flutter Module的构成产物发布到远程,然后在Native工程中远程依赖,这种依赖方式是默认的依赖方式,这样对其它开发同学来说是透明的,不影响开发流程和打包平台
上述说到的两种依赖方式,接下来主要说怎么进行这两种依赖方式的工程化管理和定制化
1. 无侵入Flutter SDK源码进行BugFix和定制化
Flutter SDK在使用时,不免会遇到一些Flutter SDK的问题或Bug,但这些问题通常是在各平台层的链接脚本中出现坑,而如果我们要兼容现有工程和扩展定制化功能,往往会直接修改Flutter SDK源码,这种侵入性的方式极不推荐,这对后续SDK的平滑升级会带来更多的成本
通常出现Bug或需要定制化的脚本往往是和平台链接时相关的,当然排除需要修改dart
层Api代码的情况下,这种只能更改源码了,不过这种出bug的几率还是比较小的,比较涉及到SDK的Api层面了。而大概率出现问题需要兼容或进行定制化的几个地方通常为下面几处:
$FLUTTER_SDK
/packages/flutter_tools/gradle/flutter.gradle
$FLUTTER_SDK
/bin/cache/artifacts/engine/android-arch/flutter.jar
$FLUTTER_MODULE
/.android/build.gradle、.android/settings.gradle
$FLUTTER_MODULE
/.android/Flutter/build.gradle
$FLUTTER_MODULE
/.ios/Flutter/Generated.xcconfig
$FLUTTER_MODULE
/.ios/Flutter/podhelper.rb
$FLUTTER_MODULE
/.ios/Podfile
$FLUTTER_SDK
/packages/flutter_tools/bin/xcode_backend.sh
作者:zhengxiaoyong
链接:https://www.jianshu.com/p/689795c49370
共同學習,寫下你的評論
評論加載中...
作者其他優質文章