Flutter混合栈管理
由于Flutter框架出色的UI渲染能力,多平台一致性,大大的提高了研发效率,降低了人力成本。越来越多的厂商开始接入Flutter,但是很多厂商都是成熟的App,完全从头使用Flutter开发应用不现实,采用混合开发则是一种非常好的切入方式。
那混合栈管理则是一个避免不了的话题。
本文从0到1的角度阐释Flutter混合栈实现思路,介绍关键技术点而忽略部分细节,让大家从全局认识Flutter混合栈管理。
一、引导问题
- 为什么同一个原生页面不能显示两个及以上的flutter页面?
- 为什么在使用flutterboost时不能用官方的Navigator的API?
二、页面栈结构分析
常见的几种flutter页面与原生页面混合的情况如下:
- 交替出现:原生页面与flutter页面交替出现
- 连续出现:多个flutter页面连续出现
- 同时出现:原生页面与flutter页面在子tab中同时出现
- 复合情况:上述情况组合出现,页面结构比较复杂
2.1 标准栈的情况
对于1,2两种情况比较简单,可以抽象成两个比较标准的栈结构,如下图:
对于Android侧:页面栈结构无需做任何改变,直接复用原生页面栈管理即可。
对于Flutter侧:页面栈结构可以由Navigator实现,也是可以直接使用现有的栈管理即可。
关键点:
- Flutter与Activity都可以复用各自的线性栈结构功能
- Flutter与Activity(或Fragment)的页面映射关系实现
- 连续多个Flutter页面使用一个原生容器优化内存
2.2 同级Tab的情况
对于情况3,多个Flutter页面同级可以分为两种情况:
- 多个Flutter需要同时显示页面内容,会同时出现,如Pad情况
由于一个FlutterEngine同一时刻只能渲染一块画布FlutterView,故如果需要显示这种页面结构,则必须使用多引擎。
如富途牛牛Pad的情况,FlutterPage1所在主屏需要一个独立的引擎渲染,FlutterPage2所在副屏使用一个独立引擎并关联其上的其他页面栈,则可以转换为标准的栈结构情况。
- 多个Flutter页面不需要同时显示页面内容,会交替出现,如手机App一级主tab,二级页面的子Tab
由于页面不会同时出现,这样的情况可以使用单引擎来实现,可以减少不必要的内存开销,也可以避开多引擎内存无法共享的问题。
由于有多个同级的FlutterPage页面,flutter侧不能简单的使用Navigator线行栈结构实现。
对于这种情况更好的做法是:抛弃强耦合的线性栈结构,用key-value的形式映射Flutter页面与原生页面,哪个原生页面在前台则渲染其对应的FlutterPage即可。
关键点:
- 在Flutter页面交替出现时单引擎如何渲染不同的Flutter页面
- 原生页面与Flutter页面实现k-v映射关系
2.3 复合情况
对于复合情况,这里只考虑Flutter页面交替出现的场景,对于同时显示的页面需要使用到多引擎,页面管理更复杂,本次暂不考虑。
这种复合情况可以看组1,2,3种情况的结合体,将上述两小节的关键点合考虑即可。
2.4 总结
综合上述关键点,得出我们需要的栈管理结构需要支持一下的关键点:
- 内存优化与共享:使用单引擎,需要解决在页面交替出现时单引擎如何渲染不同页面
- 连续多个Flutter页面:使用单个原生容器承载,内部可以使用Navigator管理多个Flutter页面
- 不连续的Flutter页面:一个Flutter页面一个原生页面(Fragment/Activity),栈结构由原生维护,原生页面与Flutter页面实现k-v映射关系(可以由Overlay实现)。
整个栈结构可以抽象成如下结构图:
三、页面功能分析
混合栈管理的主要功能是路由管理和页面生命周期管理,实现这两项能力就完成了混合栈管理的主体框架,后续可以在此基础上进行拓展,丰富使用能力。
3.1 页面导航
页面导航的基础能力:
- push:打开一个新的Flutter页面并携带参数,需要考虑内部路由情况
- pop:关闭一个Flutter页面并携带参数,需要考虑内部路由情况
打开页面可以分两种:
- 原生侧触发打开页
- Flutter侧触发打开页
路由管理:
- 注册路由
- 路由表管理
3.2 页面生命周期
页面生命周期的基础能力:
- onPageShow:页面显示
- onPageHide:页面隐藏
- onBackground:App进入后台
- onForeground:App回到前台
由于上节页面栈结构分析可知,原生页面与Flutter页面是一对多关系,由于多个连续的Flutter页面在同一个原生容器内并有FlutterNavigator管理,故可以看成一个整体。
所以,页面生命周期分发关系如下:
- 原生页面与Flutter页面一对一,将原生页面生命周期事件通知给Flutter页面即可。
- 原生页面与Flutter页面一对多,将原生页面生命周期事件通知给Flutter页面管理层,由管理层再次分发给到Flutter栈顶页面,故将一对多关系转化为一对一关系。
四、混合栈整体框架
整体架构可以分为如下几个部分:
- 页面导航接口
- Native容器管理
- 页面生命周期管理
- Dart路由管理
- 通知回调
- 双层路由栈结构
4.1 混合栈管理:Flutter Android侧类图
4.1 混合栈管理:Flutter侧类图
五、功能实现
在了解了整体的设计思路后,进入实现部分。实现部分整体分为三块:
- Dart侧双层路由栈结构、路由管理
- Native侧容器管理、页面生命周期管理
- 页面导航接口、通知回调
主要技术点:
- 单引擎交替渲染不同Flutter页面
- 非线性页面栈实现原生页面与Flutter页面实现k-v映射关系
- Dart侧内部栈结构的管理
5.1 双层路由栈结构
双层路由栈实现的关键点,非线性页面栈实现原生页面与Flutter页面实现k-v映射关系,使用k-v映射关系管理的好处是页面无顺序耦合关系,方便应付复杂的原生使用情况,谁在前台就显示谁即可。
连续多个Flutter页面,使用单个原生容器承载,内部可以使用Navigator管理多个Flutter页面。
5.1.1 外层路由栈结构Overlay
Flutter中Overlay可以很好满足我们的需要,Overlay的介绍如下:
Overlay 中维护了一个 OverlayEntry 栈,并且每一个 OverlayEntry 是自管理的
Navigator 已经创建了一个 Overlay 组件,开发者可以直接使用,并且通过 Overlay 实现了页面管理
Overlay 的应用场景有两个:实现悬浮窗口的功能,实现页面叠加
Overlay部分代码:
class Overlay extends StatefulWidget {const Overlay({Key? key,this.initialEntries = const <OverlayEntry>[],this.clipBehavior = Clip.hardEdge,}) ;}class OverlayState extends State<Overlay>{// 内部记录OverlayEntryfinal List<OverlayEntry> _entries = <OverlayEntry>[];void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }){// 更新OverlayEntry列表 setState}void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry? below, OverlayEntry? above }) {// 更新OverlayEntry列表 setState}}
OverlayEntry部分代码:
class OverlayEntry extends ChangeNotifier {OverlayEntry({required this.builder,// 构建页面bool opaque = false, //是否不透明bool maintainState = false,// 是否保持状态});void remove() { // 从Overlay中移除自己,并通知Overlay刷新}
}
通过改变Overlay中_entries的列表,可以轻松实现页面的显示、隐藏、添加和移除。
5.1.2 内层路由栈结构Navigator
由于内部路由结构都是有Flutter页面构成的,那么使用Navigator管理Flutter是一个必然的选则。
Navigator部分代码
class Navigator extends StatefulWidget {const Navigator({Key? key,this.pages = const <Page<dynamic>>[], //页面集合this.onPopPage, // 页面pop监听this.observers = const <NavigatorObserver>[], // 页面导航监听 pop、push等})}class NavigatorState extends State<Navigator>{late GlobalKey<OverlayState> _overlayKey;late List<NavigatorObserver> _effectiveObservers;Widget build(BuildContext context) {return Overlay( // Navigator内部也是Overlay实现的key: _overlayKey,initialEntries: const <OverlayEntry>[],)}bool canPop() {// 是否可以关闭当前页面,当只有一个页面时返回false,>1时返回true}void pop<T extends Object?>([ T? result ]) {// 弹出当前页面}}
Page部分代码:
abstract class Page<T> extends RouteSettings {const Page({this.key,String? name,Object? arguments,this.restorationId,}) : super(name: name, arguments: arguments);@factoryRoute<T> createRoute(BuildContext context);}
通过Navigator中管理Page就可实现页面的栈管理。
5.1.3 双路由栈结构Overlay+Navigator
由于外部路由的页面需要与原生容器绑定,需要一个唯一标识key来建立k-v关系,故需要通过扩展OverlayEntry增加key,可以提供原生页面映射Flutter页面的能力。
再者外部路由的页面需要包含内部路由的情况,外部留有创建的Flutter页面需要具有导航的能力,故需要创建一个包含Navigator的widget作为页面根,然后将第一个页面添加到Navigator中,后续连续页面的导航能力转到Navigator即可。
故可以扩展OverlayEntry,增加key字段,并返回带有Navigator的Widget管理容器。
_ContainerOverlayEntry部分代码:
// 扩展OverlayEntry增加key能力
class _ContainerOverlayEntry extends OverlayEntry {_ContainerOverlayEntry(BoostContainer container): containerUniqueId = container.pageInfo!.uniqueId!,super(builder: (ctx) => BoostContainerWidget(container: container),opaque: true,maintainState: true);///This overlay's id,which is the same as the it's related containerfinal String containerUniqueId;}
BoostContainerWidget带有Navigator:
class BoostContainerWidget extends StatefulWidget {BoostContainerWidget({LocalKey? key, required this.container}): super(key: container.key!);final BoostContainer container;@overrideState<StatefulWidget> createState() => BoostContainerState();}class BoostContainerState extends State<BoostContainerWidget> {BoostContainer get container => widget.container;@overrideWidget build(BuildContext context) {return NavigatorExt(key: container._navKey, //关联GlobalKeypages: List<Page<dynamic>>.of(container.pages),onPopPage: (route, result) {if (route.didPop(result)) {assert(route.settings is BoostPage);_updatePagesList(route.settings as BoostPage, result);return true;}return false;},observers: <NavigatorObserver>[BoostNavigatorObserver(),],);}
}
BoostContainer用于记录页面和关联Navigator:
class BoostContainer extends ChangeNotifier {// 原生传递过来的页面参数信息,包括唯一key=pageInfo!.uniqueId!final PageInfo? pageInfo;// 多个Flutter页面信息final List<BoostPage<dynamic>> _pages = <BoostPage<dynamic>>[];// 关联Navigatorfinal GlobalKey<NavigatorState> _navKey = GlobalKey<NavigatorState>();
}
整体双层页面栈结构如下如:
5.1.4 路由管理
由上一节可知,所有页面最后都会被放到Navigator中,而Navigator需要使用Page与Route,通过提供路由工程,通过路由关联。
typedef FlutterBoostRouteFactory = Route<dynamic>? Function(RouteSettings settings, String? uniqueId);final Map<String, FlutterBoostRouteFactory> _routerMap = {"/HomePage": (settings, uniqueId) {Object? map = settings.arguments;var params = Map<String, dynamic>();if (map != null && map is Map<String, dynamic>) {params = Map<String, dynamic>.from(map);}return PageRouteBuilder<dynamic>(settings: settings,pageBuilder: (_, __, ___) => HomePage(params),);},
}
5.2 Native侧容器管理
容器管理的之前需要解决单个FlutterEngine如何显示多个Flutter页面。
5.2.1 单引擎多Flutter页面渲染
Flutter当前版的设计中,FlutterEngine渲染与绘制分离的设计,单个FlutterEngine可以在多个FlutterView间切换显示。
Flutter这种设计效果类似于投影仪,FlutterEngine相当于投影器,FlutterView页面相当于幕布,FlutterView内部有多种实现方式就相当于不同的材质的幕布。
主要方法如下:
- FlutterView#detachFromFlutterEngine:FlutterView断开关联的FlutterEngine
- FlutterView#attachToFlutterEngine:FlutterView关联FlutterEngine
通过上述方法就可以在不同FlutterView间显示不同的Flutter页面了。
5.2.2 Native与Flutter页面映射
Android显示页面需要使用Activity或Fragment,Fragment需要依附于Activity存在。Fragment比Activity更清理,可以组合到页面的各个地方。
Flutter页面可能存在两种情况:
- 页面有一个实例,如课堂首页
- 页面有多个实例,如课程详情页
页面有一个实例,使用路由最为key就可建立确定的映射关系。
页面有多个实例,仅使用路由则无法区分同一类页面的映射关系。
如果要满足上述两种情况,有两种方式:
- 组合key,路由地址与另一参数组合为一个唯一key
- 单个key,为每一个页面实例生成一个独一无二的key,如UUID
所以可以采用单个key的形式,管理简单方便,然后将key(UUID)、路由地址和页面启动参数等打包传给Flutter侧用来创建具体的页面。
主要参数如下:
- key(UUID)
- 路由地址
- 是否带原生容器
- 是否透明
- 页面数据参数
具体原生侧Flutter容器接口如下:
public interface FlutterViewContainer {Activity getContextActivity();String getUrl();Map<String, Object> getUrlParams();String getUniqueId();void finishContainer(Map<String, Object> var1);default boolean isPausing() {return false;}default boolean isOpaque() {return true;}default void detachFromEngineIfNeeded() {}
}
由于上一节可以,显示一个新的Flutter页面时需要获取FlutterEngine,为了确定FlutterEngine没有被其他页面使用,故需要在启动新页面前主动释放其他页面占用的引擎。
为了知道当前是哪个FlutterViewContainer的实例持有FlutterEngine,需要记录FlutterViewContainer的实例来提提供必要的信息。
5.2.3 页面生命周期管理
在业务需求开发过程中,有很多逻辑是需要依赖页面的生命周期,而Flutter页面本身是无法感知原生页面的生命周期,故需要将原生页面的生命周期通过MethodChannel回调通知Flutter侧。
Flutter页面需的生命周期:
- onPageShow:页面显示
- onPageHide:页面隐藏
- onBackground:App进入后台
- onForeground:App回到前台
Activity生命周期:
- onPageShow:onResume页面显示
- onPageHide:onPause页面隐藏
Fragment生命周期:
- onPageShow:onResume页面显示
- onPageHide:onPause页面隐藏
Fragment生命周期往往不准确,需要借助很多其他的回调方法已经页面状态来判断。
原生切换显示的时机:
- 原生页面显示时:先将Flutter切到对应页面,然后FlutterView#attachToFlutterEngine
- 原生页面隐藏时:FlutterView#detachFromFlutterEngine
5.3 页面导航接口
5.3.1 页面导航接口实现
原生侧接口:
- openPage:打开Flutter页面
- closePage:关闭Flutter页面
- NativeRouterApi#pushNativeRoute:接收Flutter侧打开页原生面请求
- NativeRouterApi#pushFlutterRoute:接收Flutter侧打开Flutter页面请求
Flutter侧接口:
- 路由相关操作接口
- 页面生命周期相关接口
整体接口如下:
class CommonParams {String pageName;String uniqueId;Map<String, Object> arguments;bool opaque;String key;
}@HostApi()
abstract class NativeRouterApi {void pushNativeRoute(CommonParams param);void pushFlutterRoute(CommonParams param);@asyncvoid popRoute(CommonParams param);
}@FlutterApi()
abstract class FlutterRouterApi {void pushRoute(CommonParams param);void popRoute(CommonParams param);void removeRoute(CommonParams param);void onForeground(CommonParams param);void onBackground(CommonParams param);void onContainerShow(CommonParams param);void onContainerHide(CommonParams param);void onBackPressed();void onNativeResult(CommonParams param);
}
5.3.2 操作Flutter页面相关时序图
打开与关闭流程图
六、多引擎混合栈
多引擎主要是为了解决多个Flutter页面同时显示的问题,但是也会带了一些问题,如多个引擎见内存不可见,通信问题等
多引擎在实际开发中还是有需求的,如何在上述实现单引擎混合栈的逻辑中能否支持多引擎呢?当然是可以的~~
首先需要注意这里还是不能解决内存可见性问题,但是可以以引擎对象实例为key同时维护多个单引擎混合栈是很容易做到。
效果如下图:
七、总结
本文思路与实现均来源于FlutterBoost,整体实现比较清晰。
主要关注点:
- 外部页面栈使用k-v形式实现
- 单引擎在多个FlutterView之间切换逻辑
想深入研究一下实现细节的,可以跟着FlutterBoost源码过一遍能更好的加深理解!
参考文档:
Flutter Boost
Flutter Boost3.0初探
flutter_thrio
Flutter Navigator 2.0原理详解
Flutter 必知必会系列 —— 官方给的 Navigator 2.0 设计原则
一款零侵入的高效Flutter混合栈管理方案,你值得拥有!
Flutter混合栈管理相关推荐
- Flutter开发之《网易新闻客户端Flutter混合开发实践》笔记(52)
摘自:网易新闻客户端Flutter混合开发实践 引言 网易新闻项目本身很庞大,业务繁多,全部改为Flutter实现肯定是不现实的,在使用Flutter的前期阶段,我们挑选了相对独立的几个模块,在现有工 ...
- 技术干货 | Flutter 混合开发基础
导读:Flutter 支持以独立页面.甚至是 UI 片段的方式,集成到现有的应用中,即所谓的混合开发模式.本文主要谈谈 Android 平台下, Flutter 的混合开发与构建. 文|李成达 网易云 ...
- 网易新闻客户端Flutter混合开发实践
转载:http://dy.163.com/v2/article/detail/EA0O4PQ705376OPS.html Flutter简单介绍 Flutter是Google打造的UI工具包,帮助开发 ...
- flutter 应用场景_Flutter混合开发的路由栈管理
为了把 Flutter 引入到原生工程,我们需要把 Flutter 工程改造为原生工程的一个组件依赖,并以组件化的方式管理不同平台的 Flutter 构建产物,即 Android 平台使用 aar.i ...
- Android仿饿了么加减控件,Flutter + Native混合栈仿饿了么APP
前言 一个基于Flutter + Native混合开发的APP,请求数据均人为制造. 目前仅上传Android版本,iOS暂未上传 APK下载 Github地址 效果图: 实现功能: 首页 使用百度定 ...
- 码上用它开始Flutter混合开发——FlutterBoost
开源地址: https://github.com/alibaba/flutter_boost 为什么需要混合方案 具有一定规模的App通常有一套成熟通用的基础库,尤其是阿里系App,一般需要依赖很多体 ...
- 已开源|码上用它开始Flutter混合开发——FlutterBoost
为什么80%的码农都做不了架构师?>>> 为什么需要混合方案 具有一定规模的App通常有一套成熟通用的基础库,尤其是阿里系App,一般需要依赖很多体系内的基础库.那么使用Flu ...
- 阿里云混合云管理平台发布帮您管好云
6月9日, 在2020阿里云线上峰会上阿里云混合云战略正式发布:全栈建云.智能管云.极致用云.同步发布专有云敏捷版(Apsara Stack Agility). 混合云管理平台(Apsara Uni- ...
- 58同城 Flutter 混合开发探索与实践
点击"开发者技术前线",选择"星标????" 在看|星标|留言, 真爱 导语 本文主要介绍将Flutter应用到已有Native项目中混合开发遇到的问题及解决 ...
- 异构混合多云管理的需求,如何在SDN平台落地丨TF成立大会演讲实录
本文整理自华胜天成云计算研发与产品中心总经理李明军在"TF中文社区成立暨第一次全员大会"上的演讲.更多会议资料,请在"TF中文社区"公众号后台回复"成 ...
最新文章
- 英伟达开源自动驾驶AI算法,升级芯片性能7倍于Xavier
- python3基础语法-Python3的一些基础语法介绍和理解
- linux下 mysql 的root用户忘记密码解决方案
- Google学术发布2019年最有影响力的7篇论文(附下载链接)
- 使用java操作ranger,hdfs ranger授权操作,hive ranger授权操作
- linux中的改变bin级别,Linux常用命令
- VMware Workstation虚拟机窗口小,无法显示内部系统全部桌面
- 都9012年了,还有人说IntelliJ IDEA不好用?那是因为没掌握这些技巧。
- python 日志解决方案_日常Python问题的绝佳解决方案
- java 双等于 equals_在Java中等于equals vs Arrays.equals
- 戴尔PowerEdge 4路服务器全面升级 实现企业应用与核心业务工作负载的优异性能...
- html 中的特殊字符转义,html拼接字符串中特殊字符(‘ “ 等的转义问题)
- 使用代码调用Attachments(附件)
- 解决zabbix自动发现主机后主机名称是IP地址的问题
- jpress转换html5,JPress技术精讲:JPress如何做到安装后重新加载的?
- 本周AI热点回顾:AI消除马赛克神器公布;Github黑暗模式正式发布;「中国AlphaFold」创生!
- 《数学之美》读后感与商榷
- MySQL where in 用法详解
- 什么,BOM指的是物料清单?
- SpringBoot利用ZXing工具来生成二维码(简单)
热门文章
- 给个华为服务器账号和密码忘了怎么办啊,华为帐号密码忘了怎么办?华为帐号找回密码教程...
- STM32H750移植STemWin,驱动ST7789
- 蓝湖怎么切图标注_蓝湖:你们要的“自动切图”功能来了!
- 利用多线程中线程休眠----sleep实现简单的计时器以及时钟功能
- 【JavaScript】 一万字 JavaScript 笔记(详细讲解 + 代码演示 + 图解)
- JavaScript 基础(一)
- MySQL数据库复制概论
- java判断101-200之间有多少个素数_并输出所有素数_编程基础练习:题目:判断101-200之间有多少个素数,并输出所有素数。 - 菜鸟头头...
- CAPM模型和Alpha策略
- chrome Android 前进 后退,停止Chrome后退/前进两根手指滑动