概述
按个人对 FlutterBoost 学习的经验,按这样的顺序和分类来分析 FlutterBoost 路由实现的原理。
- Flutter 原生路由实现
- FlutterBoost 路由实现
- Native打开Flutter页
- Flutter页内打开Flutter页
- Flutter页打开Native页
主要以掌握原理为主。
Flutter 原生路由实现
Flutter本质上是一套渲染框架,支持"响应式编程",通过改变 Widget Tree 来渲染不同的页面。显然,不同页面间的路由(切换)也是通过变换不同的 Widget Tree 来实现的。接下来看看具体是怎么实现的?
下图展示了 Flutter 路由管理用到的关键类:
说明下核心对象的作用:
- Route: 编程定义"路由"的信息,面向开发者。例如我们最熟悉的 MeterialPageRoute,构建要打开的页面
- _RouteEntry: NavigatorState 管理路由的对象,是对 Route 的封装
- NavigatorState: 顾名思义,Flutter 路由管理的核心对象,实现路由增删逻辑
- OverlayEntry: 表示一个路由页面入口,通过 NavigatorState ,_RouteEntry 生成
- OverlayState: OverlayState 管理 OverlayEntry 是否展示和展示的顺序
- OverlayEntryWidget: 对 OverlayEntry 表示页面的封装
- _Theater: 顾名思义,经过 OverlayState 编排的页面,最终挂在这个组件上面展示出来
以上对象核心的关系可总结为2点:
- 保存路由信息的对象传递关系:Route -> _RouteEntry -> OverlayEntry -> OverlayEntryWidget
- NavigatorState 增加,删除路由对象,将结果传到 OverlayState。OverlayState 负责管理这些路由是否展示以及展示的顺序。
通过一行代码即可路由到 TargetPage:
dart
// 假设打开一个 TargetPage 的页面
Navigator.of(context).push(MeterialPageRoute(builder: (context) => TargetPage());
从上面可知,管理路由的核心对象包括 Route , NavigatorState , OverlayState ,那他们在 Widget Tree 上是什么关系呢?
先直接给出结论:Route 是 Overlay 的子组件,Overlay 是 Navigator 的子组件。
这个父子关系是怎么形成的呢?
通过分析 Navigator 和"第一个页面"是怎么挂到 Widget Tree 上的来解答。
dart
// 以创建一个最简单的 demo 为例
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
从构建 MeterialApp 开始跟踪,看第一个页面 MyHomePage 怎么通过 Navigator 添加到 Widget Tree 的。
关键步骤:
- _MeterialAppState 的 build 方法中通过 _buildWidgetApp() 方法构建 WidgetsApp,并将其挂在 Widget Tree 上
- WidgetsApp 构建 Navigator 子节点, Navigator 内部构建 Overlay 子节点,并在首次 mount 时候初始化 Overlay 要展示的第一个页面 MyHomePage。
核心代码分析:
dart
// 1. 构建 WidgetsApp
class _MaterialAppState extends State<MaterialApp> {
...
// 构建 WidgetsApp,这里重点关注 pageRouteBuilder, home 两个传参,含义注释中说明。
// 其他一些参数会对 Route 构建有作用,这里我们分析默认的构建行为,其他暂时忽略,道理都是一样的。
Widget _buildWidgetApp(BuildContext context) {
...
return WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: widget.navigatorObservers!,
// 回调这个方法构建对应的 MaterialPageRoute 对象
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
return MaterialPageRoute<T>(settings: settings, builder: builder);
},
home: widget.home, // 对应要构建的页面 MyHomePage
routes: widget.routes!, // 例子中值为空
initialRoute: widget.initialRoute, // 例子中值为空,后续构建会有 默认值
onGenerateRoute: widget.onGenerateRoute, // 例子传 null
onGenerateInitialRoutes: widget.onGenerateInitialRoutes, // 例子传 null
// 其他参数略
);
}
...
}
// 2. 构建 Navigator
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
...
@override
Widget build(BuildContext context) {
Widget? routing;
if (_usesRouterWithDelegates) {
routing = Router<Object>(
restorationScopeId: 'router',
routeInformationProvider: _effectiveRouteInformationProvider,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate!,
backButtonDispatcher: _effectiveBackButtonDispatcher,
);
// widget.home != null,走这个分支
} else if (_usesNavigator) {
assert(_navigator != null);
// 构建这个 Widget 最终挂在到 Widget Tree 上
routing = FocusScope(
debugLabel: 'Navigator Scope',
autofocus: true,
// 构建 Navigator
child: Navigator(
clipBehavior: Clip.none,
restorationScopeId: 'nav',
key: _navigator,
initialRoute: _initialRouteName,
onGenerateRoute: _onGenerateRoute, // 回调上面 pageRouteBuilder 构建MaterialPageRoute
onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
? Navigator.defaultGenerateInitialRoutes // 回调构建默认初始的路由 '/'
: (NavigatorState navigator, String initialRouteName) {
return widget.onGenerateInitialRoutes!(initialRouteName);
},
onUnknownRoute: _onUnknownRoute,
observers: widget.navigatorObservers!,
reportsRouteUpdateToEngine: true,
),
);
} else if (_usesRouterWithConfig) {
routing = Router<Object>.withConfig(
restorationScopeId: 'router',
config: widget.routerConfig!,
);
}
...
}
}
// 3.构建 Overlay
// 4.初始化根路由,并将 MyHomePage 挂在到 Widget Tree
class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
...
@override
Widget build(BuildContext context) {
return HeroControllerScope.none(
child: Listener(
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUpOrCancel,
onPointerCancel: _handlePointerUpOrCancel,
child: AbsorbPointer(
absorbing: false, // it's mutated directly by _cancelActivePointers above
child: FocusTraversalGroup(
policy: FocusTraversalGroup.maybeOf(context),
child: Focus(
focusNode: focusNode,
autofocus: true,
skipTraversal: true,
includeSemantics: false,
child: UnmanagedRestorationScope(
bucket: bucket,
// 构建 Overlay
child: Overlay(
key: _overlayKey,
clipBehavior: widget.clipBehavior,
initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
),
),
),
),
),
),
);
}
...
// 初始化根路由,并将 MyHomePage 挂在到 Widget Tree。
// 调用时机在 Navigator 首次 build 时候,调用堆栈
// #0 NavigatorState.restoreState (package:flutter/src/widgets/navigator.dart:3348:40)
// #1 RestorationMixin._doRestore (package:flutter/src/widgets/restoration.dart:912:5)
// #2 RestorationMixin.didChangeDependencies (package:flutter/src/widgets/restoration.dart:898:7)
// #3 NavigatorState.didChangeDependencies (package:flutter/src/widgets/navigator.dart:3436:11)
// #4 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5237:11)
// #5 ComponentElement.mount (package:flutter/src/widgets/framework.dart:5062:5)
// #6 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3971:16)
// #7 Element.updateChild (package:flutter/src/widgets/framework.dart:3708:18)
// #8 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5111:16)
// #9 Element.rebuild (package:flutter/src/widgets/framework.dart:4805:7)
// #10 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5068:5)
// #11 ComponentElement.mount (package:
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
...
// 4.1 情况1,widget.pages 不为空,FlutterBoost 打开页面时候会在这里初始化路由。
for (final Page<dynamic> page in widget.pages) {
// 所有的 page 构造 _RouteEntry,与前面 push 路由流程一致
final _RouteEntry entry = _RouteEntry(
page.createRoute(context),
pageBased: true,
initialState: _RouteLifecycle.add,
);
// 这里则回到前面说的 push 路由一样的流程了
_history.add(entry);
_history.addAll(_serializableHistory.restoreEntriesForPage(entry, this));
}
// MyHomePage 例子进入这个分支
if (!_serializableHistory.hasData) {
String? initialRoute = widget.initialRoute;
if (widget.pages.isEmpty) {
// 默认初始化路由 '/'
initialRoute = initialRoute ?? Navigator.defaultRouteName;
}
if (initialRoute != null) {
// 这里则回到前面说的 push 路由一样的流程了
_history.addAll(
// 这里则回调前面构建 Navigator 时候传参方法,将 '/' 与 MyHomePage 绑定起来
widget.onGenerateInitialRoutes(
this,
widget.initialRoute ?? Navigator.defaultRouteName,
).map((Route<dynamic> route) => _RouteEntry(
route,
pageBased: false,
initialState: _RouteLifecycle.add,
restorationInformation: route.settings.name != null
? _RestorationInformation.named(
name: route.settings.name!,
arguments: null,
restorationScopeId: _nextPagelessRestorationScopeId,
)
: null,
),
),
);
}
}
...
// 最后将路由页面挂在到 Overlay 上,前面已经分析过
_flushHistoryUpdates();
}
}
// 5. 与前面分析对应,在后续的 build 方法中,MyHomePage 这个页面会在 _Theater 中构建并挂载到 Widget Tree 上,渲染到屏幕上
总之,Navigator , Overlay , 第一个路由 MyHomePage 在 Widget Tree 中关系可示意如下: 最后,Flutter 原生路由的实现可归结为3点:
- Navigator 通过 push, pop 方法增删路由页面,并保存到 Overlay 中
- Overlay 通过 rearrange 来管理不展示或者展示页面的顺序
- 以上2点的调整最终体现在 Widget Tree 上。
FlutterBoost 路由实现
到这里,上面花了不小的篇幅介绍"原生路由"实现,貌似离题了。其实不然,因为 FlutterBoost 本质上是对"原生路由"的扩展,理解"原生路由"的实现对我们理解 FlutterBoost 的路由实现很有帮助;另一方面,也符合从简单到复杂的学习顺序,对提高学习的效率也是有益的。
接下来进入正题,通过 FlutterBoost 仓库自带的 demo 来学习它的路由是如何实现的。
Native打开Flutter页
首先看下 FlutterBoost 在 dart 侧的初始化,这里主要关注它与"原生路由"在构建 Widget Tree 时有什么不一样?
在继续之前,先介绍下 FlutterBoost 中的"容器"的概念:
- Native 视角:与平台相关,例如 Android 指 Activity
- Flutter 视角:就是 Flutter Page 的集合
- Native 容器与 Flutter 容器对应,可以 一对一 或者 一对多
继续看初始化流程,从 main() 方法开始跟踪,主要构建2个对象:
- FlutterBoostApp:负责路由增删管理,角色可类比 Navigator
- Overlay:负责挂载"容器"子组件或者其他路由页面
核心代码:
scala
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return FlutterBoostApp(
// 应用自己的路由映射表,供后续路由时提供映射查找
routeFactory,
// 构建 MeterialApp,参考原生的构建过程
appBuilder: (child) => MaterialApp(
home: child,
));
}
}
// 构建 FlutterBoostApp
class FlutterBoostAppState extends State<FlutterBoostApp> {
@override
void initState() {
// 调用 Native 端路由管理接口
_nativeRouterApi = NativeRouterApi();
// 调用 Flutter 端路由管理接口
_boostFlutterRouterApi = BoostFlutterRouterApi(this);
// 初始化容器,对应路由名'/'
final BoostContainer initialContainer =
_createContainer(PageInfo(pageName: widget.initialRoute));
_containers.add(initialContainer);
super.initState();
// 在 runApp 后的 scheduleWarmUpFrame() 方法中回调
WidgetsBinding.instance.addPostFrameCallback((_) {
// 往 Overlay 上挂载初始化容器 '/'
refreshOnPush(initialContainer);
...
});
}
// 构建容器挂载的 Overlay 组件
@override
Widget build(BuildContext context) {
// widget.appBuilder 由 _MyAppState 中传入
return widget.appBuilder(WillPopScope(
...
child: Listener(
// FlutterBoost 装载管理容器的 Overlay 组件
child: Overlay(
key: overlayKey, // 全局唯一,通过这个实现对 Overlay 的访问
initialEntries: const <OverlayEntry>[],
))));
}
}
类比"原生路由",FlutterBoost 初始化完成后 Widget Tree 的示意图:
这里的红框中的 Overlay 说明下,它主要用于管理"容器"的,后面的"打开 Flutter 页面"的分析可以看到。
继续分析从 Native 侧打开一个 Flutter 页面,关键步骤:
- Native 侧构造渲染容器
- Flutter 侧在对应的容器上挂载页面,生成新的 Widget Tree
以 Android 为例,打开一个新的 Native 容器,就是打开一个 FlutterBoostActivity, 通过构建 SurfaceView 或者 TextureView,提供窗口给 Flutter Engine 渲染,与 Engine 绑定。
FlutterBoost 是单引擎方案,引擎可以与不同的容器绑定。每个容器有 uniqueId 唯一标识。Flutter 页面可以挂载在不同的容器上面。如下图所示:
以 demo 中为例,分析第一个 Flutter 页面是怎么挂载到 Widget Tree ?。
分析代码之前,先看看各个核心的类的关系:
上图是是"原生路由"核心类关系的扩展,主要是增加对容器的支持:
- BoostPage: FlutterBoost 页面表示,最终也是生成 Route 表示路由信息
- BoostContainerState: 负责构建 Navigator, Overlay,管理容器下面的路由的增、删、展示,这里的处理与"原生路由"一样
- FlutterBoostAppState 与 ContainerOverlay 一起管理容器的增、删、展示
- BoostNavigator: 封装 FlutterBoost 管理路由接口给业务调用
scala
class BoostFlutterRouterApi extends FlutterRouterApi {
@override
void pushRoute(CommonParams param) {
_addInOperationQueueOrExcute(() {
appState.pushWithInterceptor(
param.pageName, true /* isFromHost */, true /* isFlutterPage */,
withContainer: true,
// 容器id,找不到则创建新的。
// demo 中第一次打开 Flutter 页面,Native 新构建容器,所以需要重新构建 Flutter 容器
uniqueId: param.uniqueId,
arguments: Map<String, dynamic>.from(
param.arguments ?? <String, dynamic>{}));
});
}
}
class FlutterBoostAppState extends State<FlutterBoostApp> {
Future<T> pushWithInterceptor<T extends Object?>(
String? name, bool isFromHost, bool isFlutterPage,
{Map<String, dynamic>? arguments,
String? uniqueId,
bool? withContainer,
bool opaque = true}) {
...
if (state?.type == InterceptorResultType.next) {
pushOption = state!.data;
if (isFromHost) {
// Native 打开 Flutter 页面,uniqueId 对应的容器不存在则构造新的
pushContainer(name,
uniqueId: pushOption.uniqueId,
isFromHost: isFromHost,
arguments: pushOption.arguments);
} else {
// Flutter 打开 Flutter 页面
if (isFlutterPage) {
return pushWithResult(pushOption.name,
uniqueId: pushOption.uniqueId,
arguments: pushOption.arguments,
withContainer: withContainer!,
opaque: opaque);
} else {
// Flutter 打开 Native 页面
final params = CommonParams()
..pageName = pushOption.name
..arguments = pushOption.arguments;
nativeRouterApi.pushNativeRoute(params);
return pendNativeResult(pushOption.name);
}
}
}
return Future<T>.value();
}
void pushContainer(String? pageName,
{String? uniqueId,
bool isFromHost = false,
Map<String, dynamic>? arguments}) {
...
// 是否已经存在
final existed = _findContainerByUniqueId(uniqueId);
if (existed != null) {
// 已经存在,则放到顶部,重新刷新啊 Widget Tree
if (topContainer?.pageInfo.uniqueId != uniqueId) {
_containers.remove(existed);
_containers.add(existed);
//move the overlayEntry which matches this existing container to the top
refreshOnMoveToTop(existed);
}
} else {
// 构造新的容器
final pageInfo = PageInfo(
pageName: pageName,
uniqueId: uniqueId ?? _createUniqueId(pageName),
arguments: arguments,
withContainer: true);
// 构造容器对象 BoostContainer 对象
final container = _createContainer(pageInfo);
final previousContainer = topContainer;
_containers.add(container);
BoostLifecycleBinding.instance
.containerDidPush(container, previousContainer);
// Add a new overlay entry with this container
refreshOnPush(container);
}
...
}
void refreshOnPush(BoostContainer container) {
ContainerOverlay.instance.refreshSpecificOverlayEntries(
container, BoostSpecificEntryRefreshMode.add);
}
}
class ContainerOverlay {
void refreshSpecificOverlayEntries(
BoostContainer container, BoostSpecificEntryRefreshMode mode) {
...
// 增加,删除,移到顶部
switch (mode) {
case BoostSpecificEntryRefreshMode.add:
// 构建顶层 Overlay 入口 ContainerOverlayEntry ,并插入队列中,
// 最终构造 BoostContainerWidget 组件
final entry = overlayEntryFactory(container);
_lastEntries.add(entry);
overlayState.insert(entry);
break;
case BoostSpecificEntryRefreshMode.remove:
...
break;
case BoostSpecificEntryRefreshMode.moveToTop:
...
break;
}
}
}
class ContainerOverlayEntry extends OverlayEntry {
ContainerOverlayEntry(BoostContainer container)
: containerUniqueId = container.pageInfo.uniqueId,
super(
builder: (ctx) => BoostContainerWidget(container: container),
opaque: true,
maintainState: true);
}
class BoostContainerState extends State<BoostContainerWidget> {
@override
Widget build(BuildContext context) {
return HeroControllerScope(
controller: HeroController(),
// 构造 Navigator 组件,后续流程与"原生路由"流程一致
child: NavigatorExt(
key: container._navKey,
// container.pages 是要打开的 Flutter 页面,最终会在"路由映射表"中构建 Route 对象
pages: List<Page<dynamic>>.of(container.pages),
));
}
}
上述代码主要做3件事:
- 构建 Flutter 侧容器,BoostContainerWidget,并将其挂载在 Overlay 下面
- BoostContainerWidget 下构造 Navigator, Overlay,用于管理这个容器下面的路由页面,这里流程与"原生路由"管理一致
- 将页面挂载到 BoostContainerWidget 下面,完成 Flutter 页面的打开
Widget Tree 在"原生路由"下的扩展示意如下:
Flutter页打开Flutter页
FlutterBoost增加了容器的概念,支持在打开 Flutter 页面的时候是否创建新的容器,通过参数控制:
- withContainer = false。不创建新的容器,在某个容器内挂载页面
- withContainer = true。创建新的容器:Native 容器,Flutter 侧容器,在容器内挂载页面
结合上一节的内容,可用下图来解释:
Flutter页打开Native页
这块与平台相关,按 Native 页面的管理的去理解即可。 以 Android 为例,startActivity(),可以打开新的 Activity 或者复用之前已打开的都可以。