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 或者复用之前已打开的都可以。

相关推荐
m0_748247801 天前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
迷雾漫步者1 天前
Flutter组件————PageView
flutter·跨平台·dart
迷雾漫步者1 天前
Flutter组件————FloatingActionButton
前端·flutter·dart
coder_pig2 天前
📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK
flutter·ubuntu·jenkins
捡芝麻丢西瓜2 天前
flutter自学笔记5- dart 编码规范
flutter·dart
恋猫de小郭2 天前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
sunly_3 天前
Flutter:导航,tab切换,顶部固定,列表分页滚动
开发语言·javascript·flutter
敲代码的小强3 天前
Flutter项目兼容鸿蒙Next系统
flutter·华为·harmonyos
Zh-jie3 天前
flutter 快速实现侧边栏
前端·javascript·flutter
truemi.734 天前
flutter --no-color pub get 超时解决方法
android·flutter