Flutter学习:Navigator2.0

jkouu 431 0

1.Navigator1.0的不足

在介绍Navigator2.0之前,我们先来说一下为什么需要Navigator2.0。Flutter团队在文章中给出了下面三个Navigator1.0的缺陷:

  1. initialRoute参数,即应用的初始页面,在应用启动后就不能更改了。如果在用户在应用中改变了设置,使得应用应该更改初始页面,Navigator1.0没有很好的方法来解决。
  2. Navigator1.0只为用户提供了高度封装的pop()、push()这样的api,用户无法直接参与路由栈的管理。而Flutter希望用户能够通过具有Flutter特色的销毁旧组件和重建新组件的方式来管理路由。
  3. 嵌套路由下,系统的回退按钮只能由跟路由响应。比如说应用拥有一个子路由栈,用户在这个子路由栈中进行了一系列的操作,然后点击系统的回退按钮,这时回退事件是被根路由响应的,结果常常是退出app或者改变整个根路由页面——这显然不是我们希望的。

我们思考一下Flutter团队给出的Navigator1.0的三个不足,很容易就会发现,它们出现的原因是相同的:Navigator1.0的封装级别太高,导致用户无法插手路由的管理,只能通过一系列高度封装的api来更新路由。

基于这一个观点,我们可以想到,Navigator2.0一定是为我们提供了直接管理路由栈的某种媒介(路由栈的管理是框架层的实现,自然不可能直接将其暴露出来供我们修改)。我们可以向通过BuildContext操作框架层的Element树一样,通过这个媒介来管理框架层的路由栈。

2.Navitator2.0的使用和实现

Navigator2.0提供了两种管理路由的方式,一种是改良后的Navigator方式,一种是新的Router方式。

2.1.Navigator方式

Navigator方式在Navigator2.0中最重大的一个改良就是引入了Page类。

2.1.1.Page的作用

顾名思义,Page这个类就是用来描述一个页面的。我们通过一段代码比较来感受一下Page的作用:

在Navigator1.0中,我们没有Page这个类,于是注册页面的代码往往是这样的:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'App Demo',
      routes: {
        '/': (context) => HomePage(),
        '/first': (context) => FirstPage(),
      },
    );
  }
}


class HomePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text(
          'Home Page',
        ),
      ),
    );
  }

}

class FirstPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text(
          'First Page',
        ),
      ),
    );
  }

}

 

而拥有了Page之后,Navigator2.0注册页面的代码是这样的:

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return Navigator(
      pages: [
        MaterialPage(
      key: Key('home'),
      name: '/',
      child: HomePage(),
    ),
    MaterialPage(
      key: Key('first'),
      name: '/first',
      child: FirstPage(),
    ),
      ],
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text(
          'Home Page',
        ),
      ),
    );
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text(
          'First Page',
        ),
      ),
    );
  }
}

比较两段代码,我们可以看到,我们传统意义上写的基于StatelessWidget或者StatefulWidget的Page,实际上只是这个Page的内容,是一个Widget。而Navigator2.0的Page是Widget的上一级,它描述的是这个Page关于路由的信息。如果要打个比方的话,就类似于TCP/UDP和IP的关系一样。Widget就好像TCP/UDP协议,Page就好像IP报文头,一个完整的页面就好像一个IP报文。平常我们只使用上层的TCP/UDP协议,就把封装好的IP报文头忽略了,久而久之就认为IP好像就是TCP/UDP了。其实TCP/UDP只是IP报文的负载,真正保证IP报文正确传输的恰恰就是IP报文头。

2.1.2.Page的使用

我们再看上面两段代码,还可以发现两个很有意思的点:第一个是Navigator1.0中注册页面实际上注册的是一个Route,而Navigator2.0中注册页面确实注册了一个Page;第二个是App从StatelessWidget变成了StatefulWidget。

我们就从这两点展开来说一下Page怎么使用的。

首先是第一条,Navigator2.0中注册的是Page而不是Route。我们可以很轻松地想到,Route作为框架体系组成之一,是不可能轻易被废除的,那么只能是Page又生成了Route。

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

  /// The key associated with this page.
  ///
  /// This key will be used for comparing pages in [canUpdate].
  final LocalKey? key;

  final String? restorationId;

  /// Whether this page can be updated with the [other] page.
  ///
  /// Two pages are consider updatable if they have same the [runtimeType] and
  /// [key].
  bool canUpdate(Page<dynamic> other) {
    return other.runtimeType == runtimeType &&
           other.key == key;
  }

  /// Creates the [Route] that corresponds to this page.
  ///
  /// The created [Route] must have its [Route.settings] property set to this [Page].
  @factory
  Route<T> createRoute(BuildContext context);

  @override
  String toString() => '${objectRuntimeType(this, 'Page')}("$name", $key, $arguments)';
}

通过查看Page的源码,我们可以看到刚才的猜想是正确的。显然,一个Page对应一个Route,那么Page就是我们在一开始提到的,Flutter在Navigator2.0中为我们提供的操作路由栈的媒介。那么具体是怎么操作呢?

刚才我们提到第二个有意思的点,App变成了StatefulWidget。没错,万能的setState来了。既然一个Page对应一个Route,那么我们只要往Pages里面加一个新的Page或者移除一个已有的Page,然后再setState一下,Flutter自然就会更新路由栈了。所以,Navigator2.0的AppState应该是下面这样的:

class _MyAppState extends State<MyApp> {
  final List<Page> _pages = [
    MaterialPage(
      key: Key('home'),
      name: '/',
      child: HomePage(),
    ),
    MaterialPage(
      key: Key('first'),
      name: '/first',
      child: FirstPage(),
    ),
  ];

  void push(Page newPage) {
    setState(() {
      _pages.add(newPage);
    });
  }

  bool _onPop(Route<dynamic> route, dynamic result) {
    setState(() {
      _pages.remove(route.settings);
    });
    return route.didPop(result);
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      pages: _pages,
      onPopPage: _onPop,
    );
  }
}

在这段代码中,我们封装的push()函数非常简单,主要看一下我们实现的_onPop函数。onPopPage是Navigator在Navigator2.0中新增的属性,顾名思义,这个属性就是接收到了回退事件后的回调函数。这个函数返回true(也即route.didPop(result)函数的默认返回值)时,系统路由栈会弹出栈顶的页面,返回false时则不会,所以我们可以根据这个函数来决定是否弹出页面,这也是Navigator2.0中用户可以直接操作路由栈的体现之一。需要注意的是,当我们决定弹出时,一定要更新Pages,即也把Pages的最后一个Page(也就是栈顶的Page)移除,否则Flutter在弹出栈顶Page后发现栈内页面与Navigator的pages不符合,还会根据Navigator的pages重新生成路由,这样pop相当于无效了。

2.2.Router方法

Router是Navigator2.0的另外一个主要的控件,提供了与Page+Navigator不同的一套路由管理方法。

const Router({
    Key key,
    this.routeInformationProvider,
    this.routeInformationParser,
    @required this.routerDelegate,
    this.backButtonDispatcher,
  })

上面是Router的构造函数,除了Key外,它有四个成员属性,其中routeInformationParser和routerDelegate是使用Router方法中必须的两个组件(虽然routeInformationParser在Router里没有required,但是在MaterialApp.router()方法和CupertinoApp.router()方法里是必须写的)。下面我们分别介绍一下这些组件。

2.2.1.RouteInformation

在介绍RouteInformationProvider和RouteInformationParser之前,我们当然要了解一下RouteInformation这个概念。

/// A piece of routing information.
///
/// The route information consists of a location string of the application and
/// a state object that configures the application in that location.
///
/// This information flows two ways, from the [RouteInformationProvider] to the
/// [Router] or from the [Router] to [RouteInformationProvider].
/// 
/// In the former case, the [RouteInformationProvider] notifies the [Router]
/// widget when a new [RouteInformation] is available. The [Router] widget takes
/// these information and navigates accordingly.
///
/// The latter case should only happen in a web application where the [Router]
/// reports route changes back to web engine.
class RouteInformation {
  const RouteInformation({this.location, this.state});

  final String? location;

  final Object? state;
}

上面是RouteInformation的源码和注释。注释是这么解释RouteInformation的:它用来描述应用当前的路由位置,是RouteInformationProvider和Router通信的媒介。当路由位置发生变化时,RouteInformationProvider会将新的RouteInformation告知Route,使其重新构建;当路由栈变化时,Router会将新的RouteInformation告知RouteInformationProvider,使其同步给Flutter的宿主。

听起来很抽象对吧?那么我们用大白话翻译一下:

RouteInformation是一个页面的Url,RouteInformationProvider与Flutter的宿主(在这个例子里是浏览器)同步页面Url,Route负责管理路由栈。当用户在浏览器的Url栏里输入新的Url后,浏览器会自动跳转的新Url对应的页面;当浏览器跳转到新的页面(比如用户点击了浏览器的回退键),浏览器的Url栏也会更新成新页面的Url。

2.2.2.RouteInformationProvider

abstract class RouteInformationProvider extends ValueListenable<RouteInformation?> {
  void routerReportsNewRouteInformation(RouteInformation routeInformation) {}
}

上面是RouteInformationProvider的源码,可以看到,这个类非常简单,就只有一个routerReportsNewRouteInformation方法。Router通过调用这个方法来通知Flutter的宿主,路由发生了变化,需要更新应用的路由位置。这个类有一个默认的实现类PlatformRouteInformationProvider,Flutter就是通过它与宿主通信更新路由位置的。

2.2.3.RouteDelegate

RouteDelegate是Router方法中的核心,实际上Router这个类只是一个外壳,真正发挥作用的只是RouteDelegate,其它的成员属性都是RouteDelegate的一个补充。它扮演着AppState在Navigator方法中的角色——管理路由状态。事实上,RouteDelegate的实现代码和Navigator方法中AppState的实现方式是很相似的:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: _MyRouteDelegate(),
    );
  }
}

class _MyRouteDelegate extends RouterDelegate<String>
    with PopNavigatorRouterDelegateMixin<String>, ChangeNotifier {
  final List<Page> _pages = [
    MaterialPage(
      key: Key('home'),
      name: '/',
      child: HomePage(),
    ),
    MaterialPage(
      key: Key('first'),
      name: '/first',
      child: FirstPage(),
    ),
  ];

  void push(Page newPage) {
    _pages.add(newPage);
    notifyListeners();
  }

  bool _onPop(Route<dynamic> route, dynamic result) {
    _pages.remove(route.settings);
    notifyListeners();
    return route.didPop(result);
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      pages: _pages,
      onPopPage: _onPop,
    );
  }

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

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

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

  @override
  Future<void> setNewRoutePath(String configuration) async {
    if (configuration == '/') {
      _pages.clear();
      _pages.add(
        MaterialPage(
          key: Key('home'),
          name: 'home page',
          child: HomePage(),
        ),
      );
    } else if (configuration == '/first') {
      _pages.clear();

      _pages.add(
        MaterialPage(
          key: Key('home'),
          name: '/',
          child: HomePage(),
        ),
      );
      _pages.add(
        MaterialPage(
          key: Key('first'),
          name: '/first',
          child: FirstPage(),
        ),
      );
    }
  }
}

可以看到,使用RouteDelegate的代码和前面使用AppState的方法几乎一模一样,只是把setState改成了notifyListeners。如果我们查看RouteDelegate的源码,我们会发现它混入了ChangeNotifier接口(其实RouteDelegate本身也和ChangeNotifier一样继承自Listenable,混入Listenable只是为了不再重新实现一遍接口),显然它是被监听的。至于是被谁监听,通过查找引用我们会发现,是被Router的State——RouterState监听的,当我们调用ChangeNotifier后,RouterState会执行setState来重新构建路由栈。

这里顺便再说一下混入的另外一个接口:PopNavigatorRouterDelegateMixin。

mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> {
  GlobalKey<NavigatorState>? get navigatorKey;

  @override
  Future<bool> popRoute() {
    final NavigatorState? navigator = navigatorKey?.currentState;
    if (navigator == null)
      return SynchronousFuture<bool>(false);
    return navigator.maybePop();
  }
}

我们看到这个类也继承了RouterDelegate,并实现了popRoute方法。popRoute方法是响应系统回退的回调,这个类实现popRoute方法的逻辑是,通过Navigator的maybePop来保证路由在没有页面的情况下不会继续pop。如果我们想自己实现这个方法,就可以不混入这个类。

2.2.4.RouteInformationParser

从上面的RouteDelegate使用实例中我们可以看到,RouteDelegate是接受泛型的,也就是说,我们重写的setRoutePath这些函数的参数configuration的类型是不一定的。既然,RouteInformationProvider和Router通过RouteInformation通信,Router真正发挥作用的成员RouteDelegate使用的是一个泛型,那么就肯定要有一个中间类把RouteInformation解析成RouteDelegate所使用的泛型,这个类就是RouteInformationParser了。

abstract class RouteInformationParser<T> {

  const RouteInformationParser();
  
  Future<T> parseRouteInformation(RouteInformation routeInformation);

  RouteInformation restoreRouteInformation(T configuration) => null;
}

上面是RouteInformationParser的源码,这个类也很简单,就是两个函数。一个将RouteInformation转成T,一个将T转成RouteInformation。显然前者是RouteInformationProvider同步消息给Router使用的,后者是Router同步消息给RouteInformationProvider使用的。值得一提的是,第二个函数不是必须要重写的,如果不重写的话,页面回退时Url就不会同步变化了。

2.2.5.BackButtonDispatcher

BackButtonDispatcher解决的就是嵌套路由响应系统回退事件的问题。当我们有两个以上的路由嵌套,遇到系统回退事件时,究竟谁应该响应呢?这个情景和事件传递的情景很像,自然解决方法也就和事件传递的解决方法很像了。

BackButtonDispatcher有两个子类:RootBackButtonDispatcher和ChildBackButtonDispatcher。我们可以很快想到,前者是这个事件传递树的根节点,负责下发事件和最终拦截事件,后者是这个事件传递树的中间节点和叶节点,可以下发事件也可以提前拦截事件。

abstract class BackButtonDispatcher extends _CallbackHookProvider<Future<bool>> {
  late final LinkedHashSet<ChildBackButtonDispatcher> _children =
    <ChildBackButtonDispatcher>{} as LinkedHashSet<ChildBackButtonDispatcher>;

  @override
  bool get hasCallbacks => super.hasCallbacks || (_children.isNotEmpty);

  @override
  Future<bool> invokeCallback(Future<bool> defaultValue) {
    if (_children.isNotEmpty) {
      final List<ChildBackButtonDispatcher> children = _children.toList();
      int childIndex = children.length - 1;

      Future<bool> notifyNextChild(bool result) {
        // If the previous child handles the callback, we return the result.
        if (result)
          return SynchronousFuture<bool>(result);
        // If the previous child did not handle the callback, we ask the next
        // child to handle the it.
        if (childIndex > 0) {
          childIndex -= 1;
          return children[childIndex]
            .notifiedByParent(defaultValue)
            .then<bool>(notifyNextChild);
        }
        // If none of the child handles the callback, the parent will then handle it.
        return super.invokeCallback(defaultValue);
      }

      return children[childIndex]
        .notifiedByParent(defaultValue)
        .then<bool>(notifyNextChild);
    }
    return super.invokeCallback(defaultValue);
  }

  ChildBackButtonDispatcher createChildBackButtonDispatcher() {
    return ChildBackButtonDispatcher(this);
  }

  void takePriority() => _children.clear();

  void deferTo(ChildBackButtonDispatcher child) {
    assert(hasCallbacks);
    _children.remove(child); // child may or may not be in the set already
    _children.add(child);
  }

  void forget(ChildBackButtonDispatcher child) => _children.remove(child);
}

上面是BackButtonDispatcher的源码,其主要逻辑就是invokeCallBack方法,这个函数也简单易懂,就是先问子节点是否处理事件,如果子节点不处理那么自己就处理(默认是自己不处理,返回defaultValue=false,如果自己想拦截就返回true)。有一个和事件分发机制不同的细节是,这里询问子节点是倒序遍历的,因为新的子节点是在list的后面的,所以要从后往前询问。

源码里其它的函数基本上都是更改优先级的,forget是不让某个子节点处理事件,deferTo是把这个子节点的优先级提到最高,takePriority是不询问子节点,自己直接处理。

至于两个子类的源码,因为都很简单,一个是绑定WidgetsBinding监听,一个是管理子节点,这里就不再介绍了。

 

 

 

发表评论 取消回复
您必须 [登录] 才能发表评论!
分享