flutter路由简析

1,203 阅读7分钟

一.什么是flutter的路由

路由是用于页面跳转的一种方式,方便管理页面之间的跳转和互相传递数据,进行交互。使用路由,我们可以轻松实现从一个页面转换到另一个页面,系统底层其实是在帮我们将小部件执行入栈出栈操作。

在Android中,我们开启一个新页面是Activity。在iOS中,我们开启一个新页面是ViewControllers。而在Flutter中,每一个页面都是小部件, 在Flutter中我们是使用了Navigator的Api实现了页面的跳转与交互。

Navigator的Api在flutter的1.22的版本中在之前的基础上进行了Navigator 2.0的更新,引入了一套全新的声明式 API来实现路由的交互。

二.flutter路由的原理

用flutter开发界面最离不开的就是路由器,只要是涉及界面跳转就需要路由功能,而flutter的ui组成全部都是widget,在布局的时候我们并没有直接使用到路由部件(Navigator),那么它是在什么时候起作用的呢,通过翻看源码可以发现是从布局的根部件MaterialApp开始一步一步引入的。

一般app的根部件为MaterialApp,而它的build方法构建了WidgetsApp部件。

Widget build(BuildContext context) {
    Widget result = _buildWidgetApp(context);
		...
    return ScrollConfiguration(
      behavior: _MaterialScrollBehavior(),
      child: HeroControllerScope(
        controller: _heroController,
        child: result,
      )
    );
  }
Widget _buildWidgetApp(BuildContext context) {
  	...
  	return WidgetsApp(
      key: GlobalObjectKey(this),
      navigatorKey: widget.navigatorKey,
      navigatorObservers: widget.navigatorObservers!,
      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
        return MaterialPageRoute<T>(settings: settings, builder: builder);
      },
      ...
    );
  }

WidgetsApp部件的build的方法会构建Navigator路由部件,要显示的内容部件都会成为Navigator的孩子部件,我们要跳转的操作一般都会用Navigator.push等方法。

  Widget build(BuildContext context) {
  	Widget? routing;
    if (_usesRouter) {
      ...
    } else if (_usesNavigator) {
      assert(_navigator != null);
      routing = Navigator(
        restorationScopeId: 'nav',
        key: _navigator,
        initialRoute: _initialRouteName,
        ...
      );
    }
  }

而Navigator部件的的build方法则最终由Overlay来显示路由的界面。

Widget build(BuildContext context) {
    ...
    return HeroControllerScope.none(
      child: Listener(
        			...
              child: Overlay(
                key: _overlayKey,
                initialEntries: _initialOverlayEntries,
        ),
      ),
    );
  }

而Overlay部件又通过路由器存储了多少个界面将界面以_OverlayEntry部件的形式加入到onstageChildren(需要绘制的路由) 集合中,而offstageChildren 是不需要绘制的部件集合。

Widget build(BuildContext context) {
    final List<Widget> children = <Widget>[];
    bool onstage = true;
    int onstageCount = 0;
    for (int i = _entries.length - 1; i >= 0; i -= 1) {
      final OverlayEntry entry = _entries[i];
      if (onstage) {
        onstageCount += 1;
        children.add(_OverlayEntryWidget(
          key: entry._key,
          entry: entry,
        ));
        if (entry.opaque)
          onstage = false;
      } else if (entry.maintainState) {
        children.add(_OverlayEntryWidget(
          key: entry._key,
          entry: entry,
          tickerEnabled: false,
        ));
      }
    }
    /// Special version of a [Stack], that doesn't layout and render the 						first [skipCount] children.
		/// The first [skipCount] children are considered "offstage".
    return _Theatre(
      skipCount: children.length - onstageCount,
      children: children.reversed.toList(growable: false),
      clipBehavior: widget.clipBehavior,
    );
  }

也就是说你的MaterialApp的home属性显示的就是当前的路由界面,如果你需要跳转到路由B界面的话,B界面会显示在home界面的上面,也就是说把home界面当做路由A,路由B界面会显示在A界面上面,所以A界面就被B界面覆盖了,从而起到了跳转的作用,这是因为路由A界面和路由B界面的父部件是Stack。

默认的第一个路由界面的获取,是在Navigator的defaultGenerateInitialRoutes方法里面实现的。

const Navigator({
   	...
    this.initialRoute,
    this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,
    ...
  }
  
//initialRouteName = widget.initialRoute ?? Navigator.defaultRouteName;
static List<Route<dynamic>> defaultGenerateInitialRoutes(NavigatorState navigator, String initialRouteName) {
    final List<Route<dynamic>?> result = <Route<dynamic>?>[];
    if (initialRouteName.startsWith('/') && initialRouteName.length > 1) {
      initialRouteName = initialRouteName.substring(1); // strip leading '/'
     	...
      }
  	...
    return result.cast<Route<dynamic>>();
  }
  
  //defaultGenerateInitialRoutes返回值赋值给onGenerateInitialRoutes
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    ...
    if (initialRoute != null) {
        _history.addAll(
          widget.onGenerateInitialRoutes(
            this,
            widget.initialRoute ?? Navigator.defaultRouteName,
          )
        );
      }
    }
  }

widget.initialRoute,就是我们自定义设置的启动页面,让路由管理器找到第一个需要渲染的路由界面,defaultRouteName默认值为“/”,而navigator的push方法就是向内部的_history列表加入不同的路由界面,最终将所有的路由界面放_initialOverlayEntries中交给Overlay最终显示出来。

综上所述路由的原理是Overlay部件通过Stack来实现的。

三.navigator1.0的使用说明

在 Flutter 中路由用法主要有两种用法:一种是在 MaterialApp 里的 routes 参数里配置定义好路由列表,也就是提前定义好要跳转的页面和名称,然后通过这个名称来执行跳转。另一种则是通过onGenerateRoute来设置路由跳转规则实现页面跳转交互,并可通过navigatorObservers实现路由的监听。

Route 在 Flutter 中主要有两种实现方法:一个是使用 MaterialPageRoute;另一个是使用 PageRouteBuilder 来构建。

MaterialPageRoute({
    // 构建页面
    @required this.builder,
    // 路由设置
    RouteSettings settings,
    // 是否保存页面状态、内容
    this.maintainState = true,
    bool fullscreenDialog = false,
  })
PageRouteBuilder({
    // 路由设置
    RouteSettings settings,
    // 目标页面
    @required this.pageBuilder,
    // 跳转过度动画设置
    this.transitionsBuilder = _defaultTransitionsBuilder,
    this.transitionDuration = const Duration(milliseconds: 300),
    ...
  })

使用PageRouteBuilder 构建不仅可以实现路由的基本配置还可以设置跳转动画效果。

页面跳转传递参数,不直接使用在 MaterialApp 的 routes 属性里静态定义的属性值而是使用页面参数配置的动态路由方法,并通过then或者await取得参数。

// 关闭页面返回数据
Navigator.pop(context, '返回数据');
// 接收返回数据
Navigator.push<String>(context,
        MaterialPageRoute(builder: (BuildContext context) {
      return ButtonSamples(title: '标题', name: '名称');
    })).then((String result) {
      //处理代码
    });

四.Navigator2.0的简述

2.0路由的升级,更多地是为了满足 Web 端复杂路由的需要,同时也是满足状态驱动界面设计的理念。即界面与行为进行分离,通过更改状态来驱动界面完成既定行为。因此,2.0路由最关键的地方就是改变如 pop、push等传统的命令式 API,界面开始通过响应用户操作去更改数据状态,页面路由跳转统一交给了RougterDelegater来完成。

Navigator 2.0 新增的声明式 API 主要包含 Page、Router 两个部分,它们通过相互配合为 Navigator 2.0 提供了强有力的基石。

Page

page含义是 “页面”,如 widget 就是 组件 一样。Widget 保存组件配置信息,框架层内置了一个createElement可以创建与之对应的 Element 实例。 Page 同样保存页面路由相关信息,框架层也存在一个createRoute方法可以创建与之对应的 Route 实例。

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

// Widget
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}
// Page
bool canUpdate(Page<dynamic> other) {
  return other.runtimeType == runtimeType &&
         other.key == key;
}

Page 类就继承自我们Navigator 1.0中使用的 RouteSettings,保存了包含路由名称(name)和路由参数 (arguments) 等信息:

abstract class Page<T> extends RouteSettings

在新的 Navigator 组件中,通过pages参数,接收 Page 对象列表

class _MyAppState extends State<MyApp> {
  final pages = [
    MyPage(
      key: Key('/'),
      name: '/',
      builder: (context) => HomeScreen(),
    ),
    ...
  ];

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

运行应用,Flutter 就会根据这里 pages 列表中的所有 Page 对象在底层的路由栈生成对应的 Route 实例,即与 pages 对应的路由页面。

当打开某个页面时,就是在 pages 中添加一个 Page 对象,系统接收到上层的 pages 改变的通知后就会 比较新的 pages 与旧的 pages,根据比较结果,Flutter 就会在底层路由栈中新生成一个 Route 实例。Navigator 组件通过 onPopPage参数,接受一个回调函数来响应页面的 pop 事件。

Router

Router 继承自 StatefulWidget,可以管理自己的状态。也即应用的路由状态pages, 当我们改变 pages 的内容或状态时, Router 就会将该状态分发给子组件,状态改变导致子组件重建应用最新的状态。Navigator 作为 Router 的子组件时,就具有感知路由状态改变的能力。

image.png

这也是Navigator 2.0 所强调的声明式 API 的核心,我们操作路由的方式并非再是 push 或者 pop,而是改变应用的状态了!

Router 主要是通过配置 RouterDelegate实现管理自己状态的功能。RouterDelegate通过在MaterialApp.router的构造函数的routerDelegate参数传入。内部通过setInitialRoutePath和setNewRoutePath实现了初始路由和新路由的定义,通过ChangeNotifier机制来做事件通知,在build方法中返回了Navigator 组件,使得Navigator可以和router一样实现状态的感知。

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

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

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

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

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

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

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

img