庖丁解牛 · 如何理解 Flutter 路由源码设计? | 创做者训练营第二期

庖丁解牛·如何理解 Flutter 路由源码设计?

学习最忌盲目,无计划,零碎的知识点没法串成系统。学到哪,忘到哪,面试想不起来。这里我整理了Flutter面试中最常问以及Flutter framework中最核心的几块知识,欢迎关注,共同进步。image.png 欢迎搜索公众号:进击的Flutter或者runflutter 里面整理收集了最详细的Flutter进阶与优化指南。关注我,探讨你的问题,获取个人最新文章~git

本期看点: 一、70行代码实现一个丐版路由 二、路由源码细节解析github

导语:

某天在公众号看到这样一个问题面试

image.png

这问题我熟啊,恰好翻译 OverlayEntries 和 Routes 进行了重建优化 里面提到,在 1.17 版本以后,当咱们打开一个新页面(Route),前一个页面将再也不从新构建markdown

为了搞清楚前一个页面为何build,我基于 1.12.13 版本写了个 demo 测试,结果发现不止是前一个页面会再次 build,前面全部的页面都会 build框架

咱们第一印象可能会以为上一页面被覆盖了就不该该再次构建了!这是发生了什么?要搞清这个问题可还真不那么容易,划分两期来分析原理。本篇会和你们从源码分析一个最熟悉的陌生人路由async


1、初识路由:一种页面切换的能力

为何先分析路由,由于问题发生在页面切换的场景下。ide

Flutter 中咱们每每经过这样一行代码svn

image.png

打开到一个新的页面 PageE,调用 Navigator.of(context).pop() 退出一个页面。 因此路由简单来讲,就是一种页面切换的能力oop

Flutter 如何实现这一能力?为了更深入理解源码设计,本期咱们换个思路,让咱们抛开如今的路由机制思考:假如 framework 移除了路由机制,你会如何实现页面切换?源码分析


2、如何实现一个丐版路由

一、设计路由容器

为了管理每一个页面的退出和进入,咱们能够设计一个路由容器进行管理,那这个容器该如何设计? 观察页面打开和关闭这两个过程,其实很是简单。打开就是目标页面覆盖了上一个页面,而退出过程则恰好相反。

untitled.gif

根据系统现有的 Widget 咱们很天然想到了 Stack,Stack 相似原生的相对布局,每一个 Widget 能够根据本身的位置叠加显示在屏幕上。只要咱们把它的每一个子 widget 都撑满,那么 Stack 每次只会显示最后一个 widget,这不就相似每次打开一个页面么。

class RouteHostState extends State<RouteHost> with TickerProviderStateMixin {
  List<Widget> pages = []; //路由中的多个页面
  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand, //每一个页面撑满屏幕
      children: pages,
    );
  }
}
复制代码

二、提供页面切换方法

由于容器基于 Stack 因此打开和关闭页面也很是简单。对于打开一个页面咱们只须要将新的页面添加到 pages 中;关闭页面,咱们只要移除最后一个便可。为了让切换过程更加流畅,能够添加一些动画转场效果。

以打开页面为例其实只需三步

Step 一、建立一个转场动画

//一、建立一个位移动画
    AnimationController animationController;
    Animation<Offset> animation;
    animationController = AnimationController(
          vsync: this, duration: Duration(milliseconds: 500));
    animation = Tween(begin: Offset(1, 0), end: Offset.zero)
          .animate(animationController);
复制代码

Step 二、将目标页面添加到 stack 中显示

//二、添加到 stack 中并显示
    pages.add(SlideTransition(
      position: animation,
      child: page,
    ));
复制代码

Step 三、开启转场动画

//三、调用 setState 并开启转场动画
    setState(() {
        animationController.forward();
    }
复制代码

是的,简单来讲只须要这三步便可完成,咱们能够看看效果 打开路由.gif

关闭页面则反过来便可。

//关闭最后一个页面
  void close() async {
      //出场动画
      await controllers.last.reverse();
      //移除最后一个页面
      pages.removeLast();
      controllers.removeLast().dispose();
  }
}
复制代码

三、让子页面使用路由能力

上面咱们提到打开关和闭页面方法都在路由容器中,那子页面如何能使用这个能力?这个问题背后实际上是 Flutter 中一个颇有意思的话题,父子节点如何数据传递?

咱们知道 Flutter 框架体系中有三棵树,在Widget、Element、Render是如何造成树结构?中熟悉了它们的构建过程。 Flutter 提供了多个方法让咱们能够访问父子节点:

abstract class BuildContext {
  ///查找父节点中的T类型的State
  T findAncestorStateOfType<T extends State>();
  ///查找父节点中的T类型的 InheritedWidget 例如 MediaQuery 等
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object aspect })
  ///遍历子元素的element对象
  void visitChildElements(ElementVisitor visitor);
  ......
}
复制代码

源码中例如咱们常使用的NavigatorMediaQueryInheritedTheme,以及不少状态管理框架也是基于这个原理实现。一样的,能够经过这样的方法将路由能力提供给子页面。

///RouteHost提供给子节点访问本身 State 的能力
  static RouteHostState of(BuildContext context) {
    return context.findAncestorStateOfType<RouteHostState>();
  }
  ///子节点借助上面的方法使用路由
  void openPage() {
    RouteHost.of(context).open(RoutePage());
  }  
复制代码

最后咱们看看实际打开和关闭的效果:

完整案例.gif

完整案例在 github.com/Nayuta403/f…;


3、理解路由源码设计

有了上面的思考,那么对于源码的设计咱们就很清晰了。 如今咱们回过头来看看路由的使用

Navigator.of(context).push(MaterialPageRoute(builder: (c) {
      return PageB();
    }));
复制代码

对比咱们设计的路由,来拆解原理。

RouteHost.of(context).open(RoutePage());
复制代码

路由容器:Navigator

对比两个方法, 其实咱们就明白了Navigator就是起到路由容器的做用。查看源码你会发现,他被嵌套在 MaterialApp 中,而且 Nagivator 内部也是经过 Stack 实现。

image.png

咱们的每个页面都是 Navigator 的子节点,天然能够经过 context 去获取它。

static NavigatorState of(BuildContext context) {
    ///获取位于根部的 Navigator
    final NavigatorState navigator = rootNavigator
        ? context.findRootAncestorStateOfType<NavigatorState>()
        : context.findAncestorStateOfType<NavigatorState>();
    return navigator;
  }
复制代码

Route:处理页面转场等设计

明白了 Navigator 以后,咱们发现每次打开页面的时候每每须要传入 PageRoute 对象,这又起到什么做用呢?

在咱们上面的设计中,为了让过渡天然,咱们在 open 方法中,手动的为每个页面添加了转场动画。 而 Flutter 中将路由切换所需的动画,交互阻断等封装成了 Route 对象。经过层次封装的形式,逐层实现了这些能力:

image.png

有了前面的思考以后,再看路由源码的设计,思路其实变得很是清晰。对于源码的学习,千万不要一开始深陷在细节中,从总体思考再拆解流程,这样方可深刻浅出。


4、源码中的细节

有了总体大框架以后,咱们能够具体梳理 Navigator.of(context).push 过程。

Future<T> push<T extends Object>(Route<T> route) {
    final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
    /// 一、新页面的路由进行添加
    route._navigator = this;
    route.install(_currentOverlayEntry); ///关键方法!!!!!!!
    _history.add(route);
    route.didPush();
    route.didChangeNext(null);
    /// 二、上一个路由的相关回调
    if (oldRoute != null) {
      oldRoute.didChangeNext(route);
      route.didChangePrevious(oldRoute);
    }
    /// 三、回调 Navigator 的观察者
    for (NavigatorObserver observer in widget.observers)
      observer.didPush(route, oldRoute);
    RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, oldRoute);
    _afterNavigation(route);
    return route.popped;
  }
复制代码

这里咱们只需关注核心的第一个过程,关键方法在:

route.install(_currentOverlayEntry); 
复制代码

这个方法被 Route 的子类重写,而且分层完成了不一样逻辑: image.png

在OverlayRoute中以下:

void install(OverlayEntry insertionPoint) {
    /// 经过 createOverlayEntries() 建立新页面的 _overlayEntries 集合
    /// 这个 _overlayEntries 集合就是咱们打开的新页面
    _overlayEntries.addAll(createOverlayEntries());
    /// 将新页面的 _overlayEntries 集合插入到 overlay 中显示
    navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
    super.install(insertionPoint);
  }
  
  Iterable<OverlayEntry> createOverlayEntries() sync* {
    /// 建立一个遮罩
    yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
    /// 建立页面实际内容,最终调用到 Route 的 builder 方法
    yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
  }
复制代码

第一行代码中的 createOverlayEntries() 方法会先建立一个zhe调用到 Route 的 builder 方法,建立咱们须要打开的页面与遮罩,以后将整个集合添加到 Overlay 中(若是不太熟悉 Overlay 将它当作一个 Stack 就行)。

/// overlay.dart
void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry below, OverlayEntry above }) {
    setState(() {
      _entries.insertAll(_insertionIndex(below, above), entries);
    });
  }
复制代码

overlay 的方法也很简单,添加页面到 _entries 调用 setState() 更新。 这个 _entries 简单来看,这个 _entries 就和咱们前面设计的 pages 相似,不过里面多了 选择渲染 的能力,咱们下一期再详细分析。


5、总结

看到这,相信你对于 Flutter 中的路由不再会感到陌生,总结下来关键有三点:

  • 一、Navigator 做为路由容器内部嵌套了 Stack 提供了页面切换的能力。
  • 二、经过context.findRootAncestorStateOfType()能够访问父节点
  • 三、Route 为咱们封装了切换时须要的其余能力

固然其中还有一些细节,例如 Overlay 是什么,页面的生命周期是如何切换,留着下期和你们在分析了。下期内容比较深,先熟悉一下 Flutter 的渲染机制收获会更多哦~

推荐阅读:


6、最后 感谢各位吴彦祖和彭于晏的点赞和关注

当咱们切换页面的时候,上一个页面默认会走如下几个生命周期:

image.png

这又是为何?必定是这样的顺序么?Flutter 生命周期到底该怎么回答? 咱们留着下一期再分析拉~

若是你以为文章写得还不错~ 点个关注、点个赞啦,彦祖~

欢迎搜索公众号:进击的Flutter或者 runflutter 里面整理收集了最详细的Flutter进阶与优化指南。关注我,获取个人最新文章~

周末参加了掘金的创做者活动技术创做者们,快来这里交做业啦 | 创做者训练营第二期 ,多位行业大牛介绍了技术写做、思惟提高、职场晋升等等心得,受益不浅。连接中有录播,一块儿提笔开启你的写做之旅吧。

相关文章
相关标签/搜索