likes
comments
collection
share

Flutter 必知必会系列 —— 官方给的 Navigator 2.0 设计原则

作者站长头像
站长
· 阅读数 13

Navigator 2.0 的改造真的是大的改造,虽然对以前的代码不会产生影响,但是我们可以通过官方给出的设计原则来看改动的范围。本来打算写一个总结,我发现这个官方的还是不错的。

往期精彩

Summary

介绍一个声明式 API 来设置 Navigator 的历史栈,并且新引入 Router 组件。Router 组件可以根据应用的状态和系统事件来配置 Navigator

Objective

文章介绍有关 Navigator 的新的 API,可以用声明式的方式来设置 navigator 的历史栈,声明式的关键是不可变数组的 Page。 我们知道 Navigator 管理的是 Route,所以 Page 会转化成 RoutePageRoute 的关系和 WidgetElement 的关系很像。文档也会介绍现有的命令式的 Navigator API(push,pop,replace等等) 是怎么重构的,能够和新的声明式 API 相互协作。

最后,文章还会介绍一个新引入的组件 —— Router,这个组件可以包裹 Navigator 组件。Router 组件配置一组 Page,然后,Navigator 组件就显示这一组 Page,当然了新的 Navigator 可以响应来自系统、应用状态的事件。在程序运行期间,如果接收到新的跳转等意图,Router 组件能够再次配置 Navigator。在点击系统的返回键的时候,Router 组件就移除栈顶的路由来实现后退的功能。同时,Router 组件也能够配置应用的 Navigator 来响应用户的输入等等。

Goals

  • 开发者能够用声明式的方式来设置和修改 Navigator 的历史页面栈
  • 现有的命令式的 API push 等不会受到影响,并且能够和新 API 兼容
  • 开发者将 Navigator 中命令式的历史堆栈转为新的声明式的方式
  • Router 组件可以包裹 Navigator 组件并且可以基于应用状态和系统事件再次配置路有栈:
  • Router 组件能够配置 Navigator 展示应用启动时初始的路由
  • 当收到展示路由的意图时,Router 能够再次配置组件 Navigator 以展示页面。
  • 用户点击系统返回键的时候,Router 能够再次配置 Navigator 组件,更新顶层的路由实现后退效果。
  • 开发者能够委托 Router 组件再次配置 Navigator 的历史路由栈,来显示新的路由以响应用户的交互
  • 开发者能够自定义路由的行为,比如名字与页面的比对
  • Routers/Navigators 能够被嵌套,按下系统后退按钮从最合适的导航器弹出路由

Background & Motivation

这一节讨论旧版 Navigator 的能力和缺陷。旧版 Navigator 中,Flutter 提供了两种方式来配置和修改Navigator 的历史路由栈:initialRoute 属性 和 命令式 API (push、pop、pushNamed、pushAndRemoveUntil 等)

Initial Route

initialRoute 参数仅仅只在第一次构建 Navigator 的时候起作用,通常是设置 Window.defaultRouteName,应用启动的时候就会显示名字对应的路由页面。但是,Navigator 构建之后,再次修改 initialRoute 参数是无效的,这种情况下,在程序运行的时候,开发者没有好的方式来修改,因为开发者不能轻易的替换路由历史栈。

命令式 API

Navigator 的命令式 API 是 Navigator 中的静态方法,静态方法中来获取 NavigatorState 的实例。这些 API 能够让开发者 push 新的路由、移除旧的路由,开发者调用这些 API 来响应用户的交互,比如:用户点击了 AppBar 的返回按钮,开发者会调用 pop 方法来移除栈顶的路由。当用户查看元素的详情页,开发者会调用 push 方法,来在栈顶添加一个新的详情页路由。正如前面所说的,命令式 API 让每一次路由栈的修改非常具有针对性,而失去了一定的灵活性,比如,开发者不能自由的修改和重新编排路有栈。因此,开发者必须扩展现有的 API 来实现某些需求。从本质上来说,这些需求或者灵活性需要的是对历史路有栈的控制。

在 Flutter 的调研中,也有开发者反馈,命令式的 API 与 Flutter 风格不匹配。在 Flutter 中,如果想要设置 Widget 的子节点显示,那么仅仅使用新的子节点来重新构建一下 Widget 就可以了,但是在 Navigator 中不能通过这种方式来实现,必须调用笨拙的命令式的 API。

Nested Navigators

旧版 Navigator 的另一个痛点是 Navigator 的嵌套,嵌套 NavigatorsTab 页面很常见,每个 Tab 下面有一个独立的 Navigator,然后独立的 Navigator 嵌套在根 Navigator 下面。嵌套的 Navigator 追踪历史路由栈。旧版的 Navigator,Flutter 仅仅只有根 navigator 关联着系统返回键,如果开发者在 Tab 内路由到某个页面,这可能会造成混乱: 点击系统返回键会返回到上一个页面,而不会返回 Tab 中的历史栈,同时也污染了全局的历史栈。

Overview

这一节总揽 Navigator 声明式 API 和新引入的 Router 组件,下一节会详细介绍实现原理。

Navigator

这一节介绍 Page 的概念,并且将 Navigator 的路由管理分为两组:Page 的路由和非 Page 的路由。

Pages

为了能够声明式的设置 Navigator 的历史栈,开发者需要通过 Navigator 构造方法来给 Navigator 提供一组 Page 对象。Page 数组是不变的,并且描述了应该放置在 Navigator 栈中的路由。Navigator 会将一个 Page 对象 inflates 成一个 路由对象。从这方面来看, Page 和 Route 的关系就像是 Widget 和 Element 的关系:Widget 和 Page 仅仅是配置。

Page 对象能够转成一个与之相应的 Route 对象,Route 对象可以放在历史路由栈中。但是并不是每一个 Route 都有一个 Page 与之对应。

开发者可以随意的实现他们自己的 Page 对象,或者使用 framework 提供的 PageBuilders。framework 使用PageBuilders 来获取其中的 Route 或者 Widget。特定的 PageBuilder 的会用 Route 包裹与之相适应的 Widget,比如 MaterialPageRoute。

在历史路由栈中,与 Pages 相对应的 Routes 的顺序与提供给 Navigator 的列表中它们对应的Pages 的顺序相同。如果设置给 Navigator 的 Page 列表发生了变化,那么新的列表会与旧的列表进行比较,并且与之对应的历史路由栈也会更新:

  • 不存在在新列表的路由 Route会从历史路由中删除掉
  • 新列表中的 Page ,如果该 Page 还没有与之匹对的 Route,会首先 inflate 成 Route,然后会被插入到历史路由栈中指定的位置
  • 历史路有栈的顺序会更新,以确保与新 Pages 列表相同

过渡代理 Transition Delegate 决定了 Route 是怎么进入退出屏幕的。

对于新增的 Page 参数,Navigator 也提供了一个新的 onPopPage 回调。Navigator 调用这个方法来让 Page 指定的路由推出。如果接受者同意回调,框架会调用路由的 didPop 方法。如果这个过程成功执行了,框架会用新的 Page 列表更新 Navigator,新的 Page 列表不再包含已经推出的 Page,并且 onPopPage 会返回 true。如果被推出的 Page 没被移除,它会视为一个新的 Page,新的 Page 会新生成一个 Route。如果 onPopPage 的接收者不想路由被推出,它需要将回调返回 false。onPopPage 回调仅仅作用于最顶层的 Page。

Pageless Routes

已经存在的命令式 API 通过 push 等方法来插入 Route,这个动作和 Page 没啥关系。为了不对现有的功能产生破坏性的影响,之前的代码还可以继续运行。在新的 framework 中,Pageless 路由会被绑定到历史路由栈中 Page 路由的下面。如果,Page 路由在历史路有栈中的位置变化了,那么所有的与之绑定的 Pageless 路由也会跟着移动到新位置,并且 Pageless 路由的相对位置不会变化。如果一个 Page 路由从历史路由栈中移除了,那么与之绑定的 Pageless 路由也会被删除。

当 Navigator 第一次插入到组件树上的时候,负责路由列表初始化的 initialRoute 参数也会生成一个 Pageless 路由列表。初始化的 initialRoute 参数生成的路由会被放置在 Page 参数生成的路由的上面。但是,新框架并不鼓励使用 initialRoute 参数,在新框架下,最好是提供一个 Page 参数,并且将 initialRoute 设置为 null。

Transition DelegateThe

当 Navigator 中 Page 被添加和移除的时候,那么过渡代理 transition delegate 决定了与之对应的 Route 应该怎么进入和退出屏幕。为了做到这一点,过渡代理做了两个事情:

  1. 当添加/移除路由时,路由是应该动画的出现/消失还是应该直接出现/消失

  2. 在相同的位置插入和删除路由的时候,在过渡期间他们应该怎么排序

我们举个例子来理解第二个问题,比如 Navigator 的 Page 列表从 [A, B] 变为 [A, C],那么在过渡上的场景上有两种可能:

  1. C 被添加(可能带有动画)在 B 之上,而 B 被从下面移除(可能带有动画)(这个移除可能会延迟到 C 的动画完成)

  2. C 被添加到 B 下面(可能有一个动画),并且 B 在它上面移除,以显示 C

第一个的视觉效果会让使用者觉得 C 被压在了 B 上,而 第二个则会让使用者觉得 B 被弹出来然后显示 C。

为了实现第一种效果,Navigator 路由栈的顺序是 [A, B, C],为了实现第二种效果,路由栈的顺序就是 [A, C, B]。 仅仅通过比对新旧 Page 列表,没有办法决定哪一种效果是想要的。因此,过渡代理就负责确定 Page 的顺序来达到指定的效果。

当 Page 列表改动的时候,过渡代理会收到一组 HistoryDiff 对象,这个对象负责记录被添加/移除的每一个位置。HistoryDiff 包含了一组排好序的路由(被添加/移除),在这个框架下,可以查看在 diff 位置之前的历史堆栈中有哪些 Routes,以及在该位置之后的历史堆栈中有哪些 Routes。而过渡代理的作用是确定被添加或者移除的 Route 是否应该动画进来或者动画出去。更近一步,过渡代理会返回一个新的历史路有栈来决定想要的顺序。路由在添加和删除列表中的相对顺序必须在合并之后的列表中保持。

过渡代理也能决定绑定到已移除路由上的 Pageless 路由应该如何离开屏幕,为了实现这样的需求,HistoryDiff 包含了一个从 “ Page 路由”到“该路由拥有的 Pageless 路由列表”的映射。只有从列表中删除的路由可以在映射中存在一个 Entry,因为新添加的路由不能拥有 Pageless 路由。过渡代理不能修改 Pageless 路由的顺序,Pageless 路由总是位于其所属的 Page Route的顶部,它们的生命周期与它的生命周期相关联。

开发者可以在 Navigator 上配置过渡代理,开发者能够为每一个 Page 提供不同的代理,这样可以实现不同的过渡效果。

如果开发者没有提供自定义的代理,框架会提供一个默认的。默认的效果就是上面讲到的第一种方式。对于每一个 HistoryDiff,它会将所有添加的路由放在被删除路由的顶部,并且当只有历史路由栈的顶部变化时,才会发生动画效果:

  • 加入最顶上的路由是被添加的,它会让路由动画进来,当动画完成时,所有其他路由将在没有动画的情况下被添加/删除。

  • 如果路由是被删除的,那么在动画开发之前,其他路由的添加和删除都不需要动画

Summary

总之,本文档建议对公共 Navigator API 进行以下更改:

  • 引入一个新的类 —— Page,它的功能是作为创建路由的蓝图
  • 为 Navigator 添加如下的属性:

List<Page> 声明式的设置路由历史 onPopPage 让 Navigator 推出一个 Page transitionDelegate 自定义页面过渡效果

Router

Router 是一个新的组件,起到了分发的作用,来打开和关闭页面。它包裹 Navigator 组件,并且根据程序的状态来配置应该显示的 Page。更进一步,Router 也会监听操作系统的事件,并且改变 Navigator 的页面显示。

使用 Router 组件的程序可能会管理应用的状态来显示内容。不是使用命令式的 API 来显示新的路由,而是处理程序的状态。Router 注册了一个程序状态的监听,并且会重新构建 Navigator 来响应状态的改变。当 Navigator 被用新的 Page 重新配置的时候,会重新生成 Route ,来显示到屏幕上。Router 的使用过程如下图所示:

Flutter 必知必会系列 —— 官方给的 Navigator 2.0 设计原则

Router 怎么获得程序状态的改变以及对状态改变的响应能够通过 routerDelegate 配置。Router 组件的使用者需要自定义该代理的实现来满足自己的需求。开发者也可以让代理来监听应用状态的改变,但是这不是必须的。

Router 也可以帮助开发者监听操作系统相关的事件,Router 支持下面的事件:

  • 程序第一次启动的初始化 route
  • 监听操作系统打开新路由的意图
  • 监听操作系统关闭路由栈中最后一个路由的请求

Router 主要是通过 routeNameProvider 代理 和 backButtonDispatcher 代理来监听事件。 routeNameParser 代理解析命名路由,routeNameParser 会将 routeNameProvider 提供的名字解析成路由数据 T,T 就是路由的范型。框架提供了这些代理的默认实现,一般情况下我们用这些就够了。默认的代理中,T 就是 RouteSetting 数组。

routeNameParser 解析的路由数据和 backButtonDispatcher 的返回按钮通知会被传递到 routerDelegate 中,routerDelegate 可能会用新的 Page 来重新 build Navigator。上面的算法中,routerDelegate 会使用通知来重新配置 app 状态并且重新构建 Navigator 来响应状态的改变。

下面的算法展示了代理的信息流:

Flutter 必知必会系列 —— 官方给的 Navigator 2.0 设计原则

routerDelegate 与 应用状态通信的部分是可选的,取决于开发者提供的 routerDelegate 的具体实现。backButtonDispatcher 和 routeNameProvider 监听事件的位置和方式(不一定是操作系统)可以通过自定义代理来实现定制。

Route Name Provider

routeNameProvider 代理决定了 Router 如何拿到想要显示的路由。它是一个 String 的 ValueListenable,并且当 Router 第一次构建的时候,它的值就是 初始化路由。当值改变的时候,Router 就会被通知并且改变 Navigator 的配置,提供给 Navigator 一组新的 Page。

从 ValueListenable 获得的字符串会被 routeNameParser 代理解析成指定的 T,解析之后的数据传递给 routerDelegate,这样可能会重新构建 Navigator。

默认的 routeNameProvider 是 ValueNotifier,包裹 Window.defaultRouteName 作为初始值并且监听 WidgetsBindingObserver.didPushRoute。 当 didPushRoute 触发的时候,routeNameProvider 的监听者就会被通知到。

默认的 routeNameProvider 可以满足大多数的场景,很少需要自定义。

Route Name Parser

routeNameParser 代理会从 routeNameProvider 中获取当前路由的字符串,并且会将 String 转成 T。routerDelegate 会用转化后的数据来配置 Navigator 的路由显示。

默认的 routeNameParser 会解析 String 为一组 RouteSettings,RouteSettings 代表了 Page,这些 Page 会被push 进入到 navigator 中。默认的代理定义的结构:/foo/bar?id=20&name=mike,这个字符串将被解析为三个 RouteSetting 路由/,/foo,和/foo/bar,每个 RouteSetting 将有参数{'id': '20', 'name': 'mike'}与之关联。

默认的 routeNameParser 是可以满足大多数场景,开发者几乎不需要自定义。

Router Delegate

router 代理是整个 Router 的核心,负责构建正确的 Navigator,代理本身是 Router 订阅的 Listenable,只要代理的信息进行了改变,Navigator 就会被重新构建。

框架病没有提供默认的实现为 Router 代理,开发者需要实现自定义的行为来响应状态和事件。

routerDelegate 也会相应系统事件:

  • 系统返回键按下的时候 backButtonDispatcher 会被通知,popRoute 就会被调用

  • Router 构建之后就会调用 setInitialRoutePath。默认来说,这个方法仅仅调用 setNewRoutePath 。

  • 当 routeNameProvider 通知之后 setNewRoutePath 就会被调用。具体的名字是 routeNameParser 解析出来的。

Router 不会对 routerDelegate 如何处理这些通知做任何假设。可能的选项包括:

  • 无视这些事件并且不做任何事情

  • 配置应用程序状态以反映这些通知所请求的更改,然后请求 Router 用重新配置的 Navigator 重新构建

  • 直接用新的 Navigator 来构建 Router

Back Button Dispatcher

backButtonDispatcher 代理会通知 Router 用户点击了系统的返回键,并且会返回到前一个路由。backButtonDispatcher 仅仅作用于有物理返回键的。

backButtonDispatcher 是 Listenable 的实现,Router 订阅了它。当 Navigator 应该 pop 的时候,backButtonDispatcher 会通知它的监听者。 当然了这种情况仅仅针对物理返回键。

dispatcher 可以不通知自己的 Router 监听器,而是通知一个子节点的 backButtonDispatcher,并让这个子节点的 Router 监听器来处理 back 按钮的按下。这个特性适用于 Tab 嵌套的情况。设计中没有限制嵌套的级别,子backButtonDispatcher也可以有另一个子节点。

framework 中有两个 backButtonDispatcher 具体的实现:适用于根 Router 的默认实现、适用于嵌套的实现。

针对根 Router 的默认实现仅仅监听 WidgetsBindingObserver.didPopRoute ,来决定是否点击了返回键。如果有一个的优先级高于根分发器,它的处理是要么通知监听者要么传递给子 backButtonDispatcher。如果多个字节点都申请了优先级,那么最后一个申请的子节点会获得通知。当子节点决定它不在想要优先级,那么它之前的会获得通知。如果没有子节点声明优先级,那么通知分发到父节点。

ChildBackButtonDispatcher 本身不会监听任何系统事件,它仅仅从父节点获得事件。为了声明优先级,它需要持有父节点的引用。为了获得引用,Routers 被设计为 InheritedWidget。Router 组件持有 backButtonDispatcher 的引用,这个引用就是 ChildBackButtonDispatcher 的父节点。

Example Usage

用新的 Navigator API 来使用 Router,开发者基本只需要实现自定义的 RouterDelegate。对于其他的代理,使用默认的就够了。这一节就演示 Router 和 Navigator API 是怎么使用的。

For this example we assume that the stocks app has three screens: 下面的例子中,APP 中有三个页面:

  • 主页展示收藏的股票列表 和 一个搜索 icon。点击列表进入详情页面,点击搜索 icon 进入搜索页面。
  • 详情页展示关注的股票的细节,页面中的返回按钮点击之后返回到前一个页面。
  • 搜索页面有搜索栏、一个返回按钮和一个搜索结果。点击结果去详情页面。点击返回按钮去前一个页面。

程序的状态是下面这样的,程序的状态是一个 ChangeNotifier,可以让 RouterDelegate 来监听改变。

class StockAppState extends ChangeNotifier {
  // If non-null: show the search page with this initial query.
  String get searchQuery;
  String _searchQuery;
  set searchQuery(String value) {
    if (value == _searchQuery) {
      return;
    }
    _searchQuery = value;
    notifyListeners();
  }


  // If non-null: Show the details page for this symbol.
  String get stockSymbol;
  String _stockSymbol;
  set stockSymbol(String value) {
    if (value == _stockSymbol) {
      return;
    }
    _stockSymbol = value;
    notifyListeners();
  }


  // Show these symbols on the home screen.
  final List<String> favoriteStockSymbols; // Loaded from e.g. a database.
}

程序的状态能够被 RouterDelegate 使用,RouterDelegate 需要传递进入 Router。其他的代理可以使用默认的。

class StockAppRouteDelegate extends RouterDelegate<List<RouteSettings>>
    with PopNavigatorRouterDelegateMixin {
  StockAppRouteDelegate(this.state) {
    state.addListener(notifyListeners);
  }


  void dispose() {
    state.removeListener(notifyListeners);
  }


  final StockAppState state;


  @override // From PopNavigatorRouterDelegateMixin.
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();


  @override
  void setNewRoutePath(List<RouteSettings> configuration) {
    if (configuration.length != 1 || configuration.single.name != '/') {
      // Don't do anything if the route is invalid.
      return;
    }
    // Update state; if this modifies the state it will call our listener,
    // which will cause a rebuild.
    state.searchQuery = configuration.single.arguments['searchQuery'];
    state.stockSymbol = configuration.single.arguments['stockSymbol'];
  }


  @override
  Widget build(BuildContext context) {
    // Return a Navigator with a list of Pages representing the current app
    // state.
    return Navigator(
      key: navigatorKey,
      onPopPage: _handlePopPage,
      pages: [
        MaterialPageBuilder(
          key: ValueKey<String>('home'),
          builder: (context) => HomePageWidget(),
        ),
        if (state.searchQuery != null)
          MaterialPageBuilder(
            key: ValueKey<String>('search'),
            builder: (context) => SearchPageWidget(),
          ),
        if (state.stockSymbol != null)
          MaterialPageBuilder(
            key: ValueKey<String>('details'),
            builder: (context) => DetailsPageWidget(),
          ),
      ],
    );
  }


  bool _handlePopPage(Route<dynamic> route, dynamic result) {
    Page page = route.settings;
    if (page.key == ValueKey<String>('home')) {
      assert(!route.willHandlePopInternally);
      // Do not pop the home page. 
      return false;
    }


    final bool result = route.didPop(result);
    assert(result);
    // Update state to remove the page in question; if this modifies the state
    // it will call our listener, which will cause a rebuild.
    if (page.key == ValueKey<String>('search')) {
      state.searchQuery = null;
      return true;
    }
    if (page.key == ValueKey<String>('details')) {
      state.stockSymbol = null;
      return true;
    }
    assert(false); // We should never be asked to pop anything else.
    return true;
  }
}

Coexistence of imperative and declarative API

正如上面提到的,下面的内容解释命令式 API 和 声明式 API。中大型的项目可以选择通过 Router 来配置 Navigator 的历史路由栈,并且只使用命令式的 API 来启动非常短暂的 Route ,比如 Dialog 和 Alert。灵活的 Router 可能在后面更加好用,因为它将更容易集成和支持新的特性,如可链接性和保存/恢复当前实例状态(例如,当操作系统由于内存不足而在后台终止应用时)到框架中。

Detailed Design

这一节介绍一些设计上的细节。

Navigator

这一节主要介绍 Page 是怎么实现的、Navigator 是如何跟踪路由状态的、Navigator 怎么和 Page 数组同步更新的。

Pages

Route 已经有了一个 RouteSetting 属性,并且 Page 基本就是 RouteSetting,因为 Page 也可以描述 Route 的配置。因此,将 Page 设计为 RouteSetting 的子类是非常有意义的。

每一个 Page 有一个可选的 Key 属性,就像 Widget 的 Key 一样,Key 用来做更新时的标志位。LocalKey 是非常有用的,因为 Page 仅仅是在一维列表中操作。

为了做到这一点,Page 必须实现 createRoute 方法。createRoute 的入参是 BuildContext,并且返回 Page 相关的 Route。当 Page 第一次添加到 Navigator 的历史路由栈时,这个方法就会被调用。这个方法返回的 Route 必须有 settings 属性,settings 属性就是 Page。

最后,Page 也实现了 canUpdate 方法,这个方法就像 Widget 的 canUpdate 方法,默认的实现是当新旧 Page 的类型和 key 相同时,返回true。方法的调用时机是给定 Navigator 新的 Page 列表时。

总结一下:

abstract class Page<T> extends RouteSettings {
  const Page({
    this.key,
    String name,
    Object arguments,
  }) : super(name: name, arguments: arguments);


  final LocalKey key;


  bool canUpdate(Page<dynamic> other) {
    return other.runtimeType == runtimeType && other.key == key;
  }


  Route<T> createRoute(BuildContext context);
}  

Route State Machine

命令式和声明式 API 都将 Route 的状态从一个状态转换到下一个状态,同时会触发状态变化的通知。比如,Navigator 被请求弹出一个 Route 时,会调用 Route.didPop() 来触发路由退出过渡,并且等待动画完成之后释放 Route。对于 Navigator,Pop 请求会将 Route 的状态从 idle 过渡到 "waiting for pop to complete" 在到 disposed。目前这些生命周期状态更改用命令式API隐式编码。

命令式 API 必须实现和声明式 API 一样的过渡。为了避免重复编码,生命周期的过渡提取到了共享方法中,并且在调用核心方法之前,两者的 API 仅仅标记路由的过渡。核心的方法就是 flushHistoryUpdates,它会执行真正的过渡,触发所有的过渡效果。

下面的算法描述了 Route 的生命周期转换。用*标记的状态是瞬间的,并且是标记状态,告诉 flushHistoryUpdates 路由下一步需要执行什么过渡。#标记的表明 Route的停留态,会一直停留到下一个事件。

Flutter 必知必会系列 —— 官方给的 Navigator 2.0 设计原则

下面的事件决定了 Route 的生命周期:

  • 解析 initial 参数生成的 Route,被添加到历史路由栈中,是 add*状态

  • push() 方法添加的 Route 是从 push* 状态开始,被 pushAndRemoveUntil() 方法移除的路由是 remove* 状态

  • pushReplacement() 方法添加的 Route 是从 pushReplace* 开始的,并且被替换之后,会是 remove* 状态

  • replace() 方法添加的路由从 replace* 状态开始,并且被替换时是 remove*

  • pop() 方法会让顶层的 Route 成为 pop* 状态

  • 通过 Page 数组添加到 Navigator 中的路由,可能会从 push*、 replace*、 或者 add* 状态 开始,具体是那一个则由过渡代理决定

  • 通过 Page 数组移除 Navigator 的路由,可能会是 pop*、 complete*、 或者 remove*,具体是那一个则由过渡代理决定

  • push 动画完成之后,就从 pushing# 状态开始往下走

  • finalizeRoute 被调用之后,表明 pop 动画已经完成了,就从 popping# 继续往下走

  • 当 Route 的所有 push 完成之后,或者顶上的 Route 是 idle 的,路线就会从 removing# 和 adding# 继续向下走,以避免可见的视觉故障。

通知 Route 它们新的上一个/下一个路由(通过didChangeNext()或didChangePrevious())被延迟,直到所有由星号指示的瞬态变化被处理。这样可以避免将即将消失的瞬态状态通知给 Route。Route 也只被告知下一个活跃的 Route 。

图中没有显示的是在 push 动画仍在运行时移除 Route 的边缘情况(它处于push #状态)。这种情况也是完全支持的 : push# 中的路由可以跳过 idle 状态,直接在 idle 后进入其中一种状态。

Route 对象和它的状态会被绑定到 RouteEntry 对象中。Navigator 的历史路由栈仅仅是 RouteEntries 数组。

Updating Pages

当 Page 数组被改变的时候,历史路由栈就会被更新。对于这种情况,Navigator 构造一个新的与 Page 数组向匹对的历史路由栈,如果 canUpdate 返回了 true ,表明新 Page 与旧 Route 可以匹对。默认,比对是 runtime 类型和 key。如果 Page 皮对了 Route,Route 的 RouteSetting 就会用新的 Page 更新。Route 的 RouteEntry 和 Pageless Route 就会被复制到新的历史路由栈中

如果旧列表中没有匹对的 Page,那么 Page 就会调用 createRoute。新生成的 Route 就会用 RouteEntry 包裹,并将其添加到新的历史路由栈中

这个过程会遍历整个 Page 数组。最后,没有匹对 Page 的 Route 会被标记为 removed ,并且也会被复用当有新的 Page 列表中包含的时候。历史路由栈中保持的 Route 可以被触发,并且等待释放

idle状态之后的路由不能再与 Page 匹配,因为它们基本上是在退出

过渡代理决定被添加和被删除的 Route 的顺序,也会决定 Route 如何过渡进入和过渡退出

Transition Delegate

从机制的角度来看,过渡代理决定被添加到历史路由栈中的 Route 是什么状态的。上面的状态图有:push*、replace* 和 add*。相似的,idle 状态之后的状态应该是啥,也是过渡代理决定的。idle 状态之后的状态是:pop*、 remove*、 and complete*。

上面提到了,合并之后的历史路由栈被分割为多个 HistoryDiff,这些 HistoryDiff 会逐个交给过渡代理。下面的例子展示了差异是如何创建的,小写字母表示 Pageless 路由,大写字母表示 Page 路由,PA 表示路由 a对应的页:

Flutter 必知必会系列 —— 官方给的 Navigator 2.0 设计原则

会产生两个 HistoryDiffs,第一个包含下面的信息:

Flutter 必知必会系列 —— 官方给的 Navigator 2.0 设计原则

过渡代理接受这些 diff,会调用 E 和 F 的方法,将它们标记为 push*、 replace*、或者 add* 之一的状态。相似的,B 、C ,以及与之关联的 Pageless 路由 x、 y、 z 也会被调用 pop*、remove*、 and complete* 中的一个方法。最后,返回一个新的列表,新的列表合并了添加和删除的路由,结果就是 [E, B, F, C]。第二个 HistoryDiff 就是下面的:

Flutter 必知必会系列 —— 官方给的 Navigator 2.0 设计原则

过渡代理处理 Diff 的方式类似,因为 "Routes after diff" 是空的,过渡代理能够推断 HistoryDiff 是历史有栈的顶部。在 HistoryDiff 中添加和删除列表以及映射中包含的 Route,基本上是 RouteEntries 的只读视图,Navigato 在内部使用它来管理历史堆栈。 只读的视图能够让过渡代理访问 Route 的状态和 RouteSettings。也暴露了一些方法来让改动 RouteEntry 的状态。其他的 RouteEntries 不能够访问其他的额外方法。

Routes & RouteSettings

现在,如果要想无动画的添加一个 Route 进入到历史路由栈,方式就是把它作为一个初始化路由。而声明式 API 实现起来就很简单了。尤其是路由被遮挡的时候,这种场景比较有意义。为了支持这一点,didAdd 方法就被添加了。和 didPush 相比,didAdd 方法是无动画的,这个方法也会用于初始化路由。initialRoute 就是已经无用的了,这一个改变。

Router

Router 和它的各种委托代理之间的 API 是完全异步的。当新的 Route 是可用的时候,routeNameProvider 会异步的通知 Router。routeNameParser 会返回一个 Future ,这个 Future 在解析完成的时候就会生成一个结果。当新的 navigator 配置是可用的时候,routerDelegate 就会通知 Router。

异步的设计让开发者有了更高的灵活性:routeNameParser 可能需要与OEM线程通信以获取更多的 Route 数据,通信是异步的,因此解析的 API 也需要是异步的。相似的,设计也作用于其他的 API 方法。

当代理可以完全同步地执行它们的工作时,那么在它们的实现中应该使用 SynchronousFuture。这将允许 Router 以完全同步的方式进行。然而,如果需要的话,Router 完全支持异步工作。

要启用正确的异步处理,在实现 Router 时必须特别注意 : 当代理返回的 Future 完成时,需要检查创建 Future 的请求是否仍然是当前请求。如果另一个新的请求已经被发出,那么 Future 的完成值应该被忽略。

Router Delegate

routerDelegate 必须实现下面的接口:

abstract class RouterDelegate<T> implements Listenable {  
     void setInitialRoutePath(T configuration); 
     void setNewRoutePath(T configuration);  
     Future<bool> popRoute();  
     Widget build(BuildContext context);
}

RouterDelegate 的任务是返回一个配置正确的 Navigator,Navigator 的配置是 Page 数组,这个数组是应该展示到屏幕上的。只要 RouterDelegate 改变了 Navigator 的配置,那么应该调用 notifyListeners() 方法。Router 组件会根据这个信号进行重新构建并且请求一个新的 Navigator 从 RouteDelegate 的 build 方法中。

routerDelegate 的其他方法会被 Router 调用,这些方法用来响应系统事件:当初始化 Route 或者解析 routeNameProvider 的新 Route,那么 setInitialRoutePath 和 setNewRoutePath 就会被调用。为了响应这些调用,代理就会通过 notifyListeners 进行通过,Navigator 就会重新构建来响应通知。

当 backButtonDispatcher 报告操作系统想要返回的时候, popRoute() 就会被调用。这可能会导致 RouterDelegate 将 pop 转发给之前的 Navigator,如果 RouterDelegate 能够处理这个 pop,那么就返回 true,否则返回 false。返回 false 之后的行为就取决 于 backButtonDispatcher 的实现

Back Button Dispatcher

As described in the Overview section, the framework will ship with two concrete BackButtonDispatcher implementations (RootBackButtonDispatcher and ChildBackButtonDispatcher), who both implement the following interface: 就像前面描述的,框架提供了两个具体的 BackButtonDispatcher 实现,BackButtonDispatcher 的接口如下:

abstract class BackButtonDispatcher implements Listenable {  
    void takePriority();  
    void deferTo(ChildBackButtonDispatcher child);  
    void forget(ChildBackButtonDispatcher child);
} 

当在 ChildBackButtonDispatcher 上调用 takpriority() 时,它会在它的父类上调用 deferTo()。父节点记住顺序列表中所有调用该方法的子节点。当父节点(或操作系统的RootBackButtonDispatcher)通知它已经按下了后退按钮时,它会通过方法调用将此通知转发给该列表中的最后一个子节点。如果列表为空,它通过调用父类中的 notifyListeners() 来通知它的 Router。如果子程序不再想接收返回按钮通知,它也可以在父程序上调用 forget()。在这种情况下,父从它的内部列表中移除子。

当在任何 BackButtonDispatcher 上调用 takpriority() 时,dispatcher 也将清除它的内部子列表,不再将后退按钮通知转发给任何子节点。

Integration

目前,Navigator 已经被集成入了 WidgetsApp 组件中,集成的原因是希望用户能够受益,我们希望 Router 能够成为 Flutter 应用中最佳的交互方式

虽然现在命令式和响应式的 API 都并存着,框架层为 MaterialApp 新增加了一个命名构造 withRouter,新的构造方法可以设置上面介绍过的代理。

新的构造方法不能设置下面的参数,这些功能可以在代理中实现:

  • navigatorKey
  • initialRoute
  • onGenerateRoute
  • onUnknownRoute
  • navigatorObservers
  • pageRouteBuilder
  • routes