Flutter Navigator2.0 彻底指南与原理解析

本文首发于微信公众号 「MeandNi」点击阅读html

Flutter 1.22 后,你们能够发现,官方对路由相关 API 的改动很大,设计文档中表示,因为传统的命令式并没有给开发者一种灵活的方式去直接管理路由栈,甚至以为已通过时了,一点也不 Flutter。git

As mentioned by a participant in one of Flutter's user studies, the API also feels outdated and not very Flutter-y.github

而 Navigator 2.0 引入了一套全新的声明式 API,全新的实现方式与调用方法与以往都大相径庭,在 Flutter Navigator 2.0 全面解析 文章中,许多读者都直呼不适应,不会用。浏览器

juejin.im_post_6880388303745449991

本文,我就来带领读者们逐步深刻 Navigator2.0 的基本原理,帮助你们探索出最佳的使用方法。微信

为何须要新的 API

在探究具体细节以前,咱们有必要了解一下 Flutter 团队为何要不惜这些代价对 Navigator API 作这么大的重构,主要有以下几点缘由。markdown

原始 API 中的 initialRoute 参数,即系统默认的初始页面,在应用运行后就不能在更改了。这种状况下,若是用户接收到一个系统通知,点击后想要从当前的路由栈状态 [Main -> Profile -> Settings] 切换到新的 [Main -> List -> Detail[id=24],Navigator1.0 并无一种优雅的实现方式实现这种效果。app

原始的命令式 Navigator API 只提供给了开发者一些很是针对性的接口,如 push、pop 等,而没有给出一种更灵活的方式让咱们直接操做路由栈。这也是我上一篇文章中提到的,这种作法其实与 Flutter 理念相违背,试想若是咱们想要改变某个 Widget 的全部子组件只须要重建全部子组件而且建立一系列新的 Widget 便可,而将此概念应用在路由中...en?当应用中存在一系列路由页面并想要更改时,咱们只能调用 push、pop 这类接口来回操做,Flutter 味道全无框架

嵌套路由下,手机设备自带的回退按钮只能由根 Navigator 响应。在目前的应用中,咱们不少场景都须要在某个子 tab 内单独管理一个子路由栈,假设有这个场景,用户在子路由栈中作一系列路由操做以后,点击系统回退按钮,消失的将是整个上层的根路由,咱们固然可使用某种措施来避免这种情况,但归咎起来,这也不该该是应用开发者应该考虑的问题。异步

因而,Navigator2.0 就过山车似的来了~async

Navigator2.0

Navigator2.0 新增的声明式 API 主要包含 Page API、Router API 两个部分,它们各自强大的功能为 Navigator2.0 提供了强有力的基石,本节我就带读者们看看它们各自的实现细节。

Page

Page 是 Navigator2.0 中最多见的类之一,从名字就能知道它的含义就是 “页面”,这就好像 Widget 就是组件同样,但 Page 与 Widget 的关系也很微妙。

Flutter 中三棵树 的概念保持一致。Widget 只保存组件配置信息,框架层内置了一个 createElement() 能够建立与之对应的 Element 实例。Page 一样只保存页面路由相关信息,框架层也存在一个 createRoute() 方法能够建立与之对应的 Route 实例。

Widget 和 Page 中也都有一个 canUpdate() 方法,帮助 Flutter 判断其是否已更新或改变:

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

// Widget
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}
复制代码

甚至连比较的条件都是运行时类型与 key

而在代码层面,Page 继承自咱们以前就用过的 RouteSettings:

abstract class Page<T> extends RouteSettings
复制代码

其中就保存包含路由名称(name,如 "/settings"),参数(arguments)等信息。

pages

下面,咱们来看一下 Page 的使用场景。在新的 Navigator 组件中,接受一个 pages 参数,它接受的就是一个 Page 对象列表,以下这段代码:

class _MyAppState extends State<MyApp> {
  final pages = [
    MyPage(
      key: Key('/'),
      name: '/',
      builder: (context) => HomeScreen(),
    ),
    MyPage(
      key: Key('/category/5'),
      name: '/category/5',
      builder: (context) => CategoryScreen(id: 5),
    ),
    MyPage(
      key: Key('/item/15'),
      name: '/item/15',
      builder: (context) => ItemScreen(id: 15),
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return //...
      Navigator(
          key: _navigatorKey,
          pages: List.of(pages),
        ),
  }
}
复制代码

此时,运行应用,Flutter 就会将这里 pages 中的全部 Page 对象在底层的路由栈生成对应的 Route 实例,即与 pages 对应的三个页面。

应用打开某个页面,就表示在 pages 中添加一个 Page 对象,系统接受接收到上层的 pages 改变后就会将新的 pages 与旧的 pages 比较,此时就会在底层路由栈中新生成一个 Route 实例,这样一个新的页面就算打开成功了。

void addPage(MyPage page) {
  setState(() => pages.add(page));
}
复制代码

Navigator 组件一样也新增了一个 onPopPage 参数,接受一个回调函数来响应页面的 pop 事件,以下面代码中的 _onPopPage:

class _MyAppState extends State<MyApp> {

  bool _onPopPage(Route<dynamic> route, dynamic result) {
    setState(() => pages.remove(route.settings));
    return route.didPop(result);
  }

  @override
  Widget build(BuildContext context) {
    print('build: $pages');
    return // ...
      Navigator(
        key: _navigatorKey,
        onPopPage: _onPopPage,
        pages: List.of(pages),
      )
  }
}
复制代码

当咱们调用 Navigator.pop() 关闭某个页面时,即能触发这个函数调用,而函数接收到的 route 对象就表示须要在 pages 中被移除的页面,在这里,咱们顺势更新 pages 列表作移除操做便可。

_onPopPage 中,若是咱们赞成关闭该页面,调用 route.didPop(result),该函数默认返回 true。

那么问题来了,咱们接收到通知可是没有更新 pages 移除相应的 Page 对象怎么办,以下这段代码:

bool _onPopPage(Route<dynamic> route, dynamic result) {
  // setState(() => pages.remove(route.settings));
  return route.didPop(result);
}
复制代码

此时,route.didPop(result) 函数触发,Flutter 会比较底层已经关闭了一个页面的路由栈中的内容和当前 Navigator 中存有的 pages,发现不一致,就会按照现有的 pages 将多余的一个 Page 当作新页面,再生成一个 Route 对象,这样底层路由栈中的内容就能随时保持与上层 pages 数据一致了。

也就是说,某个页面是否可以关闭彻底由咱们掌控,而不是单纯交给系统的 Navigator.pop() ,这里若是咱们不想关闭某个页面直接返回 false 便可:

bool _onPopPage(Route<dynamic> route, dynamic result) {
  if (...) {
    return false;
  }
  setState(() => pages.remove(route.settings));
  return route.didPop(result);
}
复制代码

须要注意的是,onPopPage 只响应路由栈顶层页面的推出,中间页面的移除不会调用这个回调函数。

这也合情合理,若是咱们想要移除非顶层页面,那么下次弹出页面时候,底层路由栈会直接与新的 pages 列表比较来作出相应改变。

要运行上述完整案例,查看完整代码:github.com/MeandNi/flu…

Flutter 框架中预先内置了 MaterialPage 和 CupertinoPage 两种 Page,分别表示 Material 和 Cupertino 风格下的页面,与 Navigator1.0 中的 MaterialPageRoute 和 CupertinoPageRoute 相呼应,它们都接受一个 child 组件表示该页面所要呈现的内容。例以下面这个例子,咱们能够直接在 pages 中使用 MaterialPage 建立页面:

List<Page> pages = <Page>[
  MaterialPage(
    key: ValueKey('VeggiesListPage'),
    child: VeggiesListScreen(
      veggies: veggies,
      onTapped: _handleVeggieTapped,
    ),
  ),
  if (show404)
    MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
  else
    if (_selectedVeggie != null)
      VeggieDetailsPage(veggie: _selectedVeggie)
];
复制代码

咱们也能够直接继承 Page 定义本身的页面类型,以下:

class MyPage extends Page {
  final Veggie veggie;

  MyPage({
    this.veggie,
  }) : super(key: ValueKey(veggie));

  Route createRoute(BuildContext context) {
    return MaterialPageRoute(
      settings: this,
      builder: (BuildContext context) {
        return VeggieDetailsScreen(veggie: veggie);
      },
    );
  }
}
复制代码

这里,咱们重写了 createRoute() 返回一个 MaterialPageRoute 对象便可。

Router

Router 是 Navigator2.0 中新增的另外一个很是重要的组件,继承自 StatefulWidget 管理本身的状态(存在一个 of 函数,与可遗传组件(InheritedWidget)搭配实现状态的统一管理,打个广告 😁,即将出版的新书《Flutter 开发之旅从南到北》第九章)。

它所管理的状态就是应用的路由状态,结合上节中提到的 Page 的概念,咱们就能够将其中的 pages 看作这里的路由状态,当咱们改变 pages 内容/状态时,Router 就会将该状态分发给子组件,状态改变致使子组件重建应用最新的状态。(第九章确实须要读者们仔细阅读,其中涵盖状态管理的知识点贯穿了 Flutter 框架层的设计)。

因此当 Navigator 做为 Router 的子组件时,就会自然具备感知路由状态改变的能力了,以下图所示:

当用户点击某个按钮就会触发相似下面这个函数的调用,该函数又会致使状态改变而重建子组件。

void _pushPage() {
  MyRouteDelegate.of(context).push('Route$_counter');
}
复制代码

Navigator2.0 所强调的声明式 API 的核心就在于此,咱们操做路由的方式并不是再是 push 或者 pop,而是改变应用的状态了!咱们须要从观念上理解声明式 API 与以往的不一样之处。

Router 代理

Router 要完成上面所说的功能主要须要经过配置 RouterDelegate(路由代理)实现。

Navigator2.0 以后,Flutter 也提供了 MaterialApp 的新构造函数 router 来帮助咱们隐式构造出全局的 Router 组件,使用方式以下:

MaterialApp.router(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
    visualDensity: VisualDensity.adaptivePlatformDensity,
  ),
  routeInformationParser: MyRouteParser(),
  routerDelegate: delegate,
)
复制代码

该构造函数接受一个 routerDelegate 参数,这里,就能够传入了咱们本身建立的 MyRouteDelegate 对象,具体代码以下:

class MyRouteDelegate extends RouterDelegate<String> with PopNavigatorRouterDelegateMixin<String>, ChangeNotifier {
  final _stack = <String>[];

  static MyRouteDelegate of(BuildContext context) {
    final delegate = Router.of(context).routerDelegate;
    assert(delegate is MyRouteDelegate, 'Delegate type must match');
    return delegate as MyRouteDelegate;
  }

  MyRouteDelegate({
    @required this.onGenerateRoute,
  });

  // ...
  @override
  Widget build(BuildContext context) {
    print('${describeIdentity(this)}.stack: $_stack');
    return Navigator(
      key: navigatorKey,
      onPopPage: _onPopPage,
      pages: [
        for (final name in _stack)
            MyPage(
              key: ValueKey(name),
              name: name,
              routeFactory: onGenerateRoute,
            ),
      ],
    );
  }
}
复制代码

上面的 MyRouteDelegate 继承自 RouterDelegate,内部能够实现它的 setInitialRoutePath、setNewRoutePath、build 与 currentConfiguration getter 四个方法,而且也混入了 PopNavigatorRouterDelegateMixin 类,它的主要做用是响应 Android 设备的回退按钮,而 ChangeNotifier 做用即是作事件通知,下文的 “实现 RouterDelegate” 中咱们就会分析这些方法各自的做用。

这里,咱们先看 MyRouteDelegate.build 方法,与上一小节同样,咱们能够经过传入 pages 和 onPopPage 参数建立一个 Navigator 组件返回,这样,当 MyRouteDelegate 对象传入 MaterialApp.router() 构造函数后,这里的 Navigator 就顺利成为了 Router 的子组件了。

大部分状况下,一个自定义的路由代理就能够这样实现完成了。

Router 事件

在应用开发中,Router 最根本的做用仍是监听各类来自系统的路由相关事件,包括:

  • 首次启动应用程序时,系统请求的初始路由。

  • 监听来自系统的新 intent,即打开一个新路由页面。

  • 监听设备回退,关闭路由栈中顶部路由。

而要想完整的响应这些事件,还得为 Router 配置 RouteNameProvider Delegate 和 BackButtonDispatcher Delegate。

最初,应用启动或者打开新页面的事件从系统发出时,会转发给应用层一个该事件相关的字符串,RouteNameParser Delegate 会将该字符串传递给 RouteNameParser,经而会解析成一个类型 T 的对象,类型 T 默认为 RouteSetting,其中就会包含传递的路由名称和参数等信息了。

相似地,用户点击设备回退按钮后,会将该事件传递给 BackButtonDispatcher Delegate。

最终,RouteNameParser 解析的对象数据和 BackButtonDispatcher Delegate 回退事件都会转发给上文中的 RouteDelegate,RouteDelegate 接受到这些事件后就会响应,执行响应的状态改变,从而致使含有 pages 的 Navigator 组件重建,在应用层中呈现最新的路由状态。

整个过程能够用下图表示:

须要知道的是,RouteNameProvider Delegate 和 BackButtonDispatcher Delegate 都有 Flutter 内置的默认实现,所以,大部分状况下,咱们并不须要考虑其中的细节,此时类型 T 默认为 RouteSetting(与 Navogator1.0 一致,包含路由信息)。

从以上部分能够看出,一系列的操做只是将最终事件传递给 RouterDelegate 而已,以后状态更新等操做均可以由咱们自定义的 RouterDelegate 决定。

实现 RouterDelegate

正如咱们上文说的,Flutter 为 RouteNameProvider Delegate 和 BackButtonDispatcher Delegate 都提供了默认实现,而 RouterDelegate 则必需要咱们手动实现,并传递给 MaterialApp.router() 构造函数才行。

咱们能够在这里完成各类业务相关的操做,RouteDelegate 自己实现自 Listenable,便可监听对象,也能够叫作被观察者,每当状态改变时,观察者们就能通知它响应该事件,从而触使 Navigator 组件重建,更新路由状态。

RouterDelegate 中的路由事件主要由下面几个函数接受:

  • backButtonDispatcher 发出回退按钮事件时,会调用 RouterDelegate 的 popRoute 方法,由混入的 PopNavigatorRouterDelegateMixin 实现。
  • 发出应用初始路由的通知时,会调用 RouterDelegate 的 setInitialRoutePath 方法,该方法接受路由名称,默认此方法会直接调用 RouterDelegate 的 setNewRoutePath 函数。
  • routeNameProvider 系统出发打开新路由页面的通知时,直接调用 setNewRoutePath 方法,参数就是由 routeNameParser 解析的结果。

所以,咱们最终就能够实现以下这样的 RouterDelegate:

class MyRouteDelegate extends RouterDelegate<String> with PopNavigatorRouterDelegateMixin<String>, ChangeNotifier {
  final _stack = <String>[];

  static MyRouteDelegate of(BuildContext context) {
    final delegate = Router.of(context).routerDelegate;
    assert(delegate is MyRouteDelegate, 'Delegate type must match');
    return delegate as MyRouteDelegate;
  }

  MyRouteDelegate({
    @required this.onGenerateRoute,
  });

  final RouteFactory onGenerateRoute;

  @override
  GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  @override
  String get currentConfiguration => _stack.isNotEmpty ? _stack.last : null;

  List<String> get stack => List.unmodifiable(_stack);

  void push(String newRoute) {
    _stack.add(newRoute);
    notifyListeners();
  }

  void pop() {
    if (_stack.isNotEmpty) {
      _stack.remove(_stack.last);
    }
    notifyListeners();
  }

  @override
  Future<void> setInitialRoutePath(String configuration) {
    return setNewRoutePath(configuration);
  }

  @override
  Future<void> setNewRoutePath(String configuration) {
    print('setNewRoutePath $configuration');
    _stack
      ..clear()
      ..add(configuration);
    return SynchronousFuture<void>(null);
  }

  bool _onPopPage(Route<dynamic> route, dynamic result) {
    if (_stack.isNotEmpty) {
      if (_stack.last == route.settings.name) {
        _stack.remove(route.settings.name);
        notifyListeners();
      }
    }
    return route.didPop(result);
  }

  @override
  Widget build(BuildContext context) {
    print('${describeIdentity(this)}.stack: $_stack');
    return Navigator(
      key: navigatorKey,
      onPopPage: _onPopPage,
      pages: [
        for (final name in _stack)
            MyPage(
              key: ValueKey(name),
              name: name,
            ),
      ],
    );
  }
}
复制代码

这里的 _stack 表示一个数据集,每一个数据会在 build 函数中建立出一个 MyPage,默认为空。应用启动时,会先调用这里的 setInitialRoutePath(String configuration) 方法,参数为 ’/‘,此时路由栈就会存在一个首页了。

完整代码,参见:github.com/MeandNi/flu…

在子组件中,咱们也可使用 MyRouteDelegate,经过以下方式打开或者关闭一个页面:

MyRouteDelegate.of(context).push('Route$_counter');

MyRouteDelegate.of(context).pop();
复制代码

与可遗传组件性质相同,这里会触发 MyRouteDelegate 中,咱们自定义的 push 和 pop 方法操做声明的路由栈,最终通知更新路由状态。

实现 RouteInformationParser

MaterialApp.router 除了须要接受路由代理 routerDelegate 这个必要参数外,还须要同时指定 routeInformationParser 参数,以下:

MaterialApp.router(
  title: 'Flutter Demo',
  routeInformationParser: MyRouteParser(), 	// 传入 MyRouteParser
  routerDelegate: delegate,
)
复制代码

该参数接收一个 RouteInformationParser 对象,定义该类一般有一个最简单直接的实现,以下:

class MyRouteParser extends RouteInformationParser<String> {
  @override
  Future<String> parseRouteInformation(RouteInformation routeInformation) {
    return SynchronousFuture(routeInformation.location);
  }

  @override
  RouteInformation restoreRouteInformation(String configuration) {
    return RouteInformation(location: configuration);
  }
}
复制代码

这里,MyRouteParser 继承自 RouteInformationParser,并重写了 parseRouteInformation()restoreRouteInformation() 两个方法。

如上文所述,parseRouteInformation() 方法的做用接受系统传递给咱们的路由信息 routeInformation 解析后,返回转发给咱们以前定义的 routerDelegate,解析后的类型为 RouteInformationParser 的泛型类型,即这里的 String。也就是说,下面这个 routerDelegate 中 setNewRoutePath() 方法的的参数 configuration 就是从这里转发而来的:

@override
Future<void> setNewRoutePath(String configuration) {
  print('setNewRoutePath $configuration');
  _stack
    ..clear()
    ..add(configuration);
  return SynchronousFuture<void>(null);
}
复制代码

restoreRouteInformation() 方法返回一个 RouteInformation 对象,表示从传入的 configuration 恢复路由信息。与 parseRouteInformation 相呼应。

例如,在浏览器中,Flutter 应用所在的标签被关闭,此时若是咱们想要恢复整个页面的路由栈则须要重写此方法,

上面 MyRouteParser 的实现,是最简单的实现方式,功能就是在 parseRouteInformation() 接受底层 RouteInformation,restoreRouteInformation() 恢复上层的 configuration。

咱们也能够为这两个方法赋能,实现更符合业务需求的逻辑,以下这代码:

import 'package:flutter/material.dart';
import 'package:flutter_navigator_v2/navigator_v2/model.dart';

class VeggieRouteInformationParser extends RouteInformationParser<VeggieRoutePath> {
  @override
  Future<VeggieRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    print("parseRouteInformation");
    final uri = Uri.parse(routeInformation.location);
    // Handle '/'
    if (uri.pathSegments.length == 0) {
      return VeggieRoutePath.home();
    }

    // Handle '/book/:id'
    if (uri.pathSegments.length == 2) {
      if (uri.pathSegments[0] != 'veggie') return VeggieRoutePath.unknown();
      var remaining = uri.pathSegments[1];
      var id = int.tryParse(remaining);
      if (id == null) return VeggieRoutePath.unknown();
      return VeggieRoutePath.details(id);
    }

    // Handle unknown routes
    return VeggieRoutePath.unknown();
  }

  @override
  RouteInformation restoreRouteInformation(VeggieRoutePath path) {
    print("restoreRouteInformation");
    if (path.isUnknown) {
      return RouteInformation(location: '/404');
    }
    if (path.isHomePage) {
      return RouteInformation(location: '/');
    }
    if (path.isDetailsPage) {
      return RouteInformation(location: '/veggie/${path.id}');
    }
    return null;
  }
}
复制代码

这里的 VeggieRouteInformationParser 继承的 RouteInformationParser 泛型类型被指定为了咱们自定义的 VeggieRoutePath,在 Navigator2.0 中咱们称这个解析后的形式为路由 model

此时 VeggieRouteInformationParser 做用就凸显出来了,它在 parseRouteInformation() 方法中接受系统的 RouteInformation 信息后就能够转换成咱们上层熟悉的 VeggieRoutePath model 对象。VeggieRoutePath 类内容以下:

class VeggieRoutePath {
  final int id;
  final bool isUnknown;

  VeggieRoutePath.home()
      : id = null,
        isUnknown = false;

  VeggieRoutePath.details(this.id) : isUnknown = false;

  VeggieRoutePath.unknown()
      : id = null,
        isUnknown = true;

  bool get isHomePage => id == null;

  bool get isDetailsPage => id != null;
}
复制代码

此时,在 RouterDelegate<VeggieRoutePath> 中,咱们就能够根据该对象作路由状态的更新了。

最佳实践

Navigator 2.0 与以往不一样的方面主要体如今,将路由状态转换成了应用自己的状态,给了开发者更大的自由与想象空间,此后,咱们能够将路由逻辑及其状态的管理与咱们的业务逻辑紧密相连,造成本身的一套方案,相信这又会是之后 Flutter 体系中一块大主题。

上述说起的全部代码包含三个案例,分别是:

源码地址:github.com/MeandNi/flu…

新书预售 🔥

image-20201108214042932

终于能够宣布啦,个人新书 《Fluter 开发之旅从南到北》 终于在异步社区开始预售了!里面涵盖了 Flutter 各种进阶知识点,包括三棵树原理,布局约束、自渲染组件、状态管理等等,全书配套开源代码:github.com/MeandNi/flu… ,正式发布以后也会有专门的文章介绍,有须要的同窗能够先关注起来 😊 但愿能对 Flutter 社区有所贡献。

预售地址:item.jd.com/10024203424…,以后会在各大书店同步发售,欢迎你们关注公众号「MeandNi」,留意以后的送书活动以及最新高质量 Flutter 技术文章。

相关文章
相关标签/搜索