FlutterBoost 路由实现原理分析

概述

按个人对 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 的。

关键步骤:

  1. _MeterialAppState 的 build 方法中通过 _buildWidgetApp() 方法构建 WidgetsApp,并将其挂在 Widget Tree
  2. 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 , 第一个路由 MyHomePageWidget Tree 中关系可示意如下: 最后,Flutter 原生路由的实现可归结为3点:

  1. Navigator 通过 push, pop 方法增删路由页面,并保存到 Overlay 中
  2. Overlay 通过 rearrange 来管理不展示或者展示页面的顺序
  3. 以上2点的调整最终体现在 Widget Tree 上。

FlutterBoost 路由实现

到这里,上面花了不小的篇幅介绍"原生路由"实现,貌似离题了。其实不然,因为 FlutterBoost 本质上是对"原生路由"的扩展,理解"原生路由"的实现对我们理解 FlutterBoost 的路由实现很有帮助;另一方面,也符合从简单到复杂的学习顺序,对提高学习的效率也是有益的。

接下来进入正题,通过 FlutterBoost 仓库自带的 demo 来学习它的路由是如何实现的。

Native打开Flutter页

首先看下 FlutterBoost 在 dart 侧的初始化,这里主要关注它与"原生路由"在构建 Widget Tree 时有什么不一样?

在继续之前,先介绍下 FlutterBoost 中的"容器"的概念:

  1. Native 视角:与平台相关,例如 Android 指 Activity
  2. Flutter 视角:就是 Flutter Page 的集合
  3. Native 容器与 Flutter 容器对应,可以 一对一 或者 一对多

继续看初始化流程,从 main() 方法开始跟踪,主要构建2个对象:

  1. FlutterBoostApp:负责路由增删管理,角色可类比 Navigator
  2. 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 页面,关键步骤:

  1. Native 侧构造渲染容器
  2. 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件事:

  1. 构建 Flutter 侧容器,BoostContainerWidget,并将其挂载在 Overlay 下面
  2. BoostContainerWidget 下构造 Navigator, Overlay,用于管理这个容器下面的路由页面,这里流程与"原生路由"管理一致
  3. 将页面挂载到 BoostContainerWidget 下面,完成 Flutter 页面的打开

Widget Tree 在"原生路由"下的扩展示意如下:

Flutter页打开Flutter页

FlutterBoost增加了容器的概念,支持在打开 Flutter 页面的时候是否创建新的容器,通过参数控制:

  • withContainer = false。不创建新的容器,在某个容器内挂载页面
  • withContainer = true。创建新的容器:Native 容器,Flutter 侧容器,在容器内挂载页面

结合上一节的内容,可用下图来解释:

Flutter页打开Native页

这块与平台相关,按 Native 页面的管理的去理解即可。 以 Android 为例,startActivity(),可以打开新的 Activity 或者复用之前已打开的都可以。

相关推荐
LawrenceLan8 小时前
Flutter 零基础入门(九):构造函数、命名构造函数与 this 关键字
开发语言·flutter·dart
一豆羹9 小时前
macOS 环境下 ADB 无线调试连接失败、Protocol Fault 及端口占用的深度排查
flutter
行者969 小时前
OpenHarmony上Flutter粒子效果组件的深度适配与实践
flutter·交互·harmonyos·鸿蒙
行者9612 小时前
Flutter与OpenHarmony深度集成:数据导出组件的实战优化与性能提升
flutter·harmonyos·鸿蒙
小雨下雨的雨12 小时前
Flutter 框架跨平台鸿蒙开发 —— Row & Column 布局之轴线控制艺术
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨12 小时前
Flutter 框架跨平台鸿蒙开发 —— Center 控件之完美居中之道
flutter·ui·华为·harmonyos·鸿蒙
小雨下雨的雨13 小时前
Flutter 框架跨平台鸿蒙开发 —— Icon 控件之图标交互美学
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨13 小时前
Flutter 框架跨平台鸿蒙开发 —— Placeholder 控件之布局雏形美学
flutter·ui·华为·harmonyos·鸿蒙系统
行者9614 小时前
OpenHarmony Flutter弹出菜单组件深度实践:从基础到高级的完整指南
flutter·harmonyos·鸿蒙
前端不太难14 小时前
Flutter / RN / iOS,在长期维护下的性能差异本质
flutter·ios