由于Flutter框架出色的UI渲染能力,多平台一致性,大大的提高了研发效率,降低了人力成本。越来越多的厂商开始接入Flutter,但是很多厂商都是成熟的App,完全从头使用Flutter开发应用不现实,采用混合开发则是一种非常好的切入方式。
那混合栈管理则是一个避免不了的话题。
本文从0到1的角度阐释Flutter混合栈实现思路,介绍关键技术点而忽略部分细节,让大家从全局认识Flutter混合栈管理。
常见的几种flutter页面与原生页面混合的情况如下:
对于1,2两种情况比较简单,可以抽象成两个比较标准的栈结构,如下图:
对于Android侧:页面栈结构无需做任何改变,直接复用原生页面栈管理即可。
对于Flutter侧:页面栈结构可以由Navigator实现,也是可以直接使用现有的栈管理即可。
关键点:
由于一个FlutterEngine同一时刻只能渲染一块画布FlutterView,故如果需要显示这种页面结构,则必须使用多引擎。
如富途牛牛Pad的情况,FlutterPage1所在主屏需要一个独立的引擎渲染,FlutterPage2所在副屏使用一个独立引擎并关联其上的其他页面栈,则可以转换为标准的栈结构情况。
由于页面不会同时出现,这样的情况可以使用单引擎来实现,可以减少不必要的内存开销,也可以避开多引擎内存无法共享的问题。
由于有多个同级的FlutterPage页面,flutter侧不能简单的使用Navigator线行栈结构实现。
对于这种情况更好的做法是:抛弃强耦合的线性栈结构,用key-value的形式映射Flutter页面与原生页面,哪个原生页面在前台则渲染其对应的FlutterPage即可。
关键点:
对于复合情况,这里只考虑Flutter页面交替出现的场景,对于同时显示的页面需要使用到多引擎,页面管理更复杂,本次暂不考虑。
这种复合情况可以看组1,2,3种情况的结合体,将上述两小节的关键点合考虑即可。
综合上述关键点,得出我们需要的栈管理结构需要支持一下的关键点:
整个栈结构可以抽象成如下结构图:
混合栈管理的主要功能是路由管理和页面生命周期管理,实现这两项能力就完成了混合栈管理的主体框架,后续可以在此基础上进行拓展,丰富使用能力。
页面导航的基础能力:
打开页面可以分两种:
路由管理:
页面生命周期的基础能力:
由于上节页面栈结构分析可知,原生页面与Flutter页面是一对多关系,由于多个连续的Flutter页面在同一个原生容器内并有FlutterNavigator管理,故可以看成一个整体。
所以,页面生命周期分发关系如下:
整体架构可以分为如下几个部分:
在了解了整体的设计思路后,进入实现部分。实现部分整体分为三块:
主要技术点:
双层路由栈实现的关键点,非线性页面栈实现原生页面与Flutter页面实现k-v映射关系,使用k-v映射关系管理的好处是页面无顺序耦合关系,方便应付复杂的原生使用情况,谁在前台就显示谁即可。
连续多个Flutter页面,使用单个原生容器承载,内部可以使用Navigator管理多个Flutter页面。
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>{
// 内部记录OverlayEntry
final 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的列表,可以轻松实现页面的显示、隐藏、添加和移除。
由于内部路由结构都是有Flutter页面构成的,那么使用Navigator管理Flutter是一个必然的选则。
Navigator部分代码
class Navigator extends StatefulWidget {
const Navigator({
Key? key,
this.pages = const >[], //页面集合
this.onPopPage, // 页面pop监听
this.observers = const [], // 页面导航监听 pop、push等
})
}
class NavigatorState extends State{
late GlobalKey _overlayKey;
late List _effectiveObservers;
Widget build(BuildContext context) {
return Overlay( // Navigator内部也是Overlay实现的
key: _overlayKey,
initialEntries: const [],
)
}
bool canPop() {
// 是否可以关闭当前页面,当只有一个页面时返回false,>1时返回true
}
void pop([ T? result ]) {
// 弹出当前页面
}
}
Page部分代码:
abstract class Page extends RouteSettings {
const Page({
this.key,
String? name,
Object? arguments,
this.restorationId,
}) : super(name: name, arguments: arguments);
@factory
Route createRoute(BuildContext context);
}
通过Navigator中管理Page就可实现页面的栈管理。
由于外部路由的页面需要与原生容器绑定,需要一个唯一标识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 container
final String containerUniqueId;
}
BoostContainerWidget带有Navigator:
class BoostContainerWidget extends StatefulWidget {
BoostContainerWidget({LocalKey? key, required this.container})
: super(key: container.key!);
final BoostContainer container;
@override
State createState() => BoostContainerState();
}
class BoostContainerState extends State {
BoostContainer get container => widget.container;
@override
Widget build(BuildContext context) {
return NavigatorExt(
key: container._navKey, //关联GlobalKey
pages: List>.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: [
BoostNavigatorObserver(),
],
);
}
}
BoostContainer用于记录页面和关联Navigator:
class BoostContainer extends ChangeNotifier {
// 原生传递过来的页面参数信息,包括唯一key=pageInfo!.uniqueId!
final PageInfo? pageInfo;
// 多个Flutter页面信息
final List> _pages = >[];
// 关联Navigator
final GlobalKey _navKey = GlobalKey();
}
整体双层页面栈结构如下如:
由上一节可知,所有页面最后都会被放到Navigator中,而Navigator需要使用Page与Route,通过提供路由工程,通过路由关联。
typedef FlutterBoostRouteFactory = Route? Function(
RouteSettings settings, String? uniqueId);
final Map _routerMap = {
"/HomePage": (settings, uniqueId) {
Object? map = settings.arguments;
var params = Map();
if (map != null && map is Map) {
params = Map.from(map);
}
return PageRouteBuilder(
settings: settings,
pageBuilder: (_, __, ___) => HomePage(params),
);
},
}
容器管理的之前需要解决单个FlutterEngine如何显示多个Flutter页面。
Flutter当前版的设计中,FlutterEngine渲染与绘制分离的设计,单个FlutterEngine可以在多个FlutterView间切换显示。
Flutter这种设计效果类似于投影仪,FlutterEngine相当于投影器,FlutterView页面相当于幕布,FlutterView内部有多种实现方式就相当于不同的材质的幕布。
主要方法如下:
通过上述方法就可以在不同FlutterView间显示不同的Flutter页面了。
Android显示页面需要使用Activity或Fragment,Fragment需要依附于Activity存在。Fragment比Activity更清理,可以组合到页面的各个地方。
Flutter页面可能存在两种情况:
页面有一个实例,使用路由最为key就可建立确定的映射关系。
页面有多个实例,仅使用路由则无法区分同一类页面的映射关系。
如果要满足上述两种情况,有两种方式:
所以可以采用单个key的形式,管理简单方便,然后将key(UUID)、路由地址和页面启动参数等打包传给Flutter侧用来创建具体的页面。
主要参数如下:
具体原生侧Flutter容器接口如下:
public interface FlutterViewContainer {
Activity getContextActivity();
String getUrl();
Map getUrlParams();
String getUniqueId();
void finishContainer(Map var1);
default boolean isPausing() {
return false;
}
default boolean isOpaque() {
return true;
}
default void detachFromEngineIfNeeded() {
}
}
由于上一节可以,显示一个新的Flutter页面时需要获取FlutterEngine,为了确定FlutterEngine没有被其他页面使用,故需要在启动新页面前主动释放其他页面占用的引擎。
为了知道当前是哪个FlutterViewContainer的实例持有FlutterEngine,需要记录FlutterViewContainer的实例来提提供必要的信息。
在业务需求开发过程中,有很多逻辑是需要依赖页面的生命周期,而Flutter页面本身是无法感知原生页面的生命周期,故需要将原生页面的生命周期通过MethodChannel回调通知Flutter侧。
Flutter页面需的生命周期:
Activity生命周期:
Fragment生命周期:
Fragment生命周期往往不准确,需要借助很多其他的回调方法已经页面状态来判断。
原生切换显示的时机:
原生侧接口:
Flutter侧接口:
整体接口如下:
class CommonParams {
String pageName;
String uniqueId;
Map arguments;
bool opaque;
String key;
}
@HostApi()
abstract class NativeRouterApi {
void pushNativeRoute(CommonParams param);
void pushFlutterRoute(CommonParams param);
@async
void 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);
}
打开与关闭流程图
多引擎主要是为了解决多个Flutter页面同时显示的问题,但是也会带了一些问题,如多个引擎见内存不可见,通信问题等
多引擎在实际开发中还是有需求的,如何在上述实现单引擎混合栈的逻辑中能否支持多引擎呢?当然是可以的~~
首先需要注意这里还是不能解决内存可见性问题,但是可以以引擎对象实例为key同时维护多个单引擎混合栈是很容易做到。
效果如下图:
本文思路与实现均来源于FlutterBoost,整体实现比较清晰。
主要关注点:
想深入研究一下实现细节的,可以跟着FlutterBoost源码过一遍能更好的加深理解!
参考文档:
Flutter Boost
Flutter Boost3.0初探
flutter_thrio
Flutter Navigator 2.0原理详解
Flutter 必知必会系列 —— 官方给的 Navigator 2.0 设计原则
一款零侵入的高效Flutter混合栈管理方案,你值得拥有!