Flutter 是一个基于 Dart 的移动开发平台,旨在帮助开发者在 iOS 和 Android 两个平台上开发高质量的跨平台应用,并在去年12 月的 Flutter Live 2018 大会上正式发布 1.0 正式版。
最近我也对 Flutter 进行了一个初步的了解和实践,过程中踩了许多坑,查了许多资料,使得自己对 Flutter 有了更进一步的理解和思考。
在本文中,我将分享自己在 Flutter 实践中的那些心路历程,主要涉及 Flutter 和 Native 混合工程开发的一些理解,希望能对大家有所帮助。
# 关于工程依赖问题的理解
总会遇到的几种工程类型
首先,明确一下在 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 | def flutterRoot = localProperties.getProperty('flutter.sdk') // 获取 Flutter SDK 的路径 |
flutter.gradle 中做了些事情,来看看:
1 | buildscript { |
主要的工作都在 FlutterPlugin 中,首先是找到对应平台的 Jar 文件:
1 | String flutterRootPath = resolveProperty(project, "flutter.sdk", System.env.FLUTTER_ROOT) |
Flutter/bin/cache/artifacts/engine 目录如下:
1 | // 如果没有找到 jar 文件,则使用「 flutter precache 」命令生成 |
引入三方 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/ |
来看 Gradle 中对 Plugin 包依赖的处理方法:
1 | // 从 .flutter-plugins 文件中读取所有的三方依赖 Plugins |
# 混合工程页面栈的问题
Native 中被熟知的栈管理
Android 中通过 startActivity 启动 Activity,iOS 中的 navigationController 启动 ViewController,各自也都实现了一套页面栈机制,并且有丰富的路由管理三方库,这些都是 Native 开发者所熟知的,这里不在赘述。
Flutter 项目中的页面路由
简单场景的用法
Flutter 中通过 Navigator 中心化的管理者来对页面栈进行管理,用法简洁、清晰。首先是对「路由名」和「控件」的对应关系进行声明:
1 | new MaterialApp( |
接下来可以进行 push 操作进行新页面的加载:
1 | new RaisedButton( |
对于页面栈切换的相关方法调用,参数可以是路由名称,也可以是构造的 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 的实体类存在,可以对其进行管理操作。
在 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 封装
2、Flutter 引擎单例化
3、连续 Flutter 页面在一个 Activity 展示
有没有做好的轮子
阿里闲鱼出了个 hybrid_stack_manager,运用上述方案 2 和 3 结合的方式
有坑吗?有一些
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 的方法
事件监听
EventChannel 是 Flutter 提供的事件流监听机制,类似 Android 中的 Handler
基本消息传递
对于字符串和半结构化的信息,可以使用 BasicMessageChannel 进行消息的传递
# 如何工程化开发
组件分离
1、对于依赖 Native 的组件,单独由 Flutter Plugin 完成
2、对于 UI 等纯 Dart 组件,可以由 Dart Packages 完成
工程隔离实施思路
1、Flutter SDK 要单独拉一份
2、Flutter SDK 需要 wrap,像 Gradlew 一样,避免版本差异
# 还想深入了解?
想对本文中提到一些技术点更加深入了解,可继续查阅下列几篇文章: