阅读 1748

Flutter 路由我定

相关阅读

基于 Navigator1.0 :

前言

Flutter 1.22 发布带来了 Navigator2.0 , 给了开发者更多选择,你可以灵活地管理路由栈,可以处理在浏览器里面输入的情况,也可以嵌套多个 Navigator,虽然仍然有缺点,但是基本上可以做到 路由我定。下面跟我一起走进 Flutter 路由的世界。本文源码版本为 Flutter Stable 1.22.6

路由基础

Navigator

负责整个路由栈, 在结构上面其实是一个 Overlay 有点类似 Stack,大家经常用它来做 toast,比如 oktoast,其实每个页面就是一个 OverlayEntry.

RouteSettings

用来保存路由名字和参数

Page

Navigator2.0 中出现,继承于 RouteSettings 。主要负责自己创建 Route 以及提一个 key,这个 key 是后面 Page 变化判断的依据,注意这个 key 在通常情况下应该为唯一的 key 。

Route

主要负责处理跳转的动画,保存 RouteSettings (即路由的名字和参数)。也是 OverlayEntry,Navigator,_RouteEntry 的纽带。

  • push 方法中,Route 直接传递进来,赋值给 _RouteEntry 增加到 _history
  Future<T> push<T extends Object>(Route<T> route) {
    _history.add(_RouteEntry(route, initialState: _RouteLifecycle.push));
    _flushHistoryUpdates();
    _afterNavigation(route);
    return route.popped;
  }
复制代码
  • NavigatorState.build 中 Overlay 的 initialEntries 的值等于 _history 中全部 _RouteEntry.route 的 OverlayEntry
  Iterable<OverlayEntry> get _allRouteOverlayEntries sync* {
    for (final _RouteEntry entry in _history)
      yield* entry.route.overlayEntries;
  }
 
 Overlay(
   key: _overlayKey,
    initialEntries: overlay == null ?  _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
   ),
复制代码

RouteTransitionRecord / _RouteEntry

后者继承前者,记录每个路由的状态

RouteTransitionRecord 中有以下属性和方法,它们都在 TransitionDelegate.resolve 方法中设置

isWaitingForEnteringDecision 标记路由是否等待进入屏幕 isWaitingForExitingDecision 标记路由是否等待离开屏幕

方法进出动画返回参数
markForPush()N/A
markForAdd()N/A
markForPop([dynamic result])
markForComplete([dynamic result])
markForRemove()

TransitionDelegate / DefaultTransitionDelegate

RouteTransitionRecordTransitionDelegate.resolve 中进行设置.

1.当 Pages 变化的时候触发更新,执行了 NavigatorState.didUpdateWidget 2.在这个方法中,去对比了新旧 Pages (我们前面说的 K 新增了一个 key,就在这里发挥了作用) 3.resolve 中判断哪些是新增,哪些是要移除的。

DefaultTransitionDelegate 中的大概逻辑如下

新Pages旧Pages状态
A=>BA=>B=>CC markForPop
A=>CA=>B=>CB markForComplete
A=>B=>C=>DA=>B=>CD markForPush
A=>B=>D=>CA=>B=>CD markForAdd

NavigatorObserver

用于监控 push,pop,replace,remove 路由的情况,以及 ios 平台上面左滑退出页面。通常我们可以在这个里面做页面进出埋点,以及解决混合开发中 Flutter与原生 ios 左滑退出冲突。

class NavigatorObserver {
  NavigatorState get navigator => _navigator;
  NavigatorState _navigator;
  
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { }

  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { }

  void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) { }

  void didReplace({ Route<dynamic> newRoute, Route<dynamic> oldRoute }) { }

  void didStartUserGesture(Route<dynamic> route, Route<dynamic> previousRoute) { }

  void didStopUserGesture() { }
}
复制代码

Navigator 1.0

这个大家都用了很久了,相信都是非常了解的了,推荐使用命名路由,并且在 onGenerateRoute 回调中统一管理路由。之前法法路由注解也是基于 Navigator 1.0 的,不懂的可以去回顾下。

Navigator 2.0

先看一下 Navigator 的最新构造有了一些什么变化。

  const Navigator({
    Key key,
    // Navigator 2.0 的新东西,由它间接把路由栈暴露给用户
    this.pages = const <Page<dynamic>>[],
    // 当使用代码 pop 或者按浏览器后退按钮的时候的回调,这个时候用户可以自行处理逻辑
    this.onPopPage,
    this.initialRoute,
    this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,
    this.onGenerateRoute,
    this.onUnknownRoute,
    // 由于 pages 是暴露给用户了,所以这里可以自己设置页面的过度动画状态,见上面 [TransitionDelegate] 部分。一般来说直接用默认的就好了
    this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),
    // 是否通知给引擎,主要是在 Web 上面,告诉浏览器 URL 的改变,同步地址
    this.reportsRouteUpdateToEngine = false,
    this.observers = const <NavigatorObserver>[],
  }) 
复制代码

Navigator 栗子

  1. 我们准备一下 Page,简单的实现下 createRoute 方法
class MyPage extends Page<void> {
  const MyPage({
    @required LocalKey key,
    @required String name,
    @required this.widget,
    Object arguments,
  }) : super(
          key: key,
          name: name,
          arguments: arguments,
        );

  final Widget widget;
  @override
  Route<void> createRoute(BuildContext context) {
    return MaterialPageRoute<void>(
      settings: this,
      builder: (BuildContext context) => widget,
    );
  }
}
复制代码
  1. 准备 pages,这就是我们的路由栈,初始化的一个 MainPage。要注意的是,key 必须是一个 唯一的 key。
  final List<MyPage> _pages = <MyPage>[
    MyPage(
        name: 'MainPage', widget: const TestPage('MainPage'), key: UniqueKey()),
  ];
复制代码
  1. 使用 Navigator,值得注意的是
  • pages 应该是一个新的集合,这样在 NavigatorState.didUpdateWidget 中才会判断不同,并且更新
  • onPopPage 回调可以根据自身的情况,进行操作,最后 setState,通知 pages 变化。通过调用 navigatorKey.currentState.pop() 或者 点击 Appbar 返回按钮都会触发该回调
    Navigator(
      reportsRouteUpdateToEngine: true,
      key: navigatorKey,
      pages: _pages.toList(),
      onPopPage: (Route<dynamic> route, dynamic result) {
        if (_pages.length > 1) {
          _pages.removeLast();
          setState(() {});
          return route.didPop(result);
        }
        return false;
      },
    ), 
复制代码
  1. 现在你就可以任意去操作路由栈了,下面举几个例子。

新增一个页面,相当于 push

    _pages.add(
      MyPage(
          name: 'MainPageA',
          widget: const TestPage('MainPageA'),
          key: UniqueKey()),
    );
    setState(() {});
复制代码

移除最后一个,相当于 pop

    if (_pages.length > 1) {
      _pages.removeLast();
      setState(() {});
    }
复制代码

直接使用 NavigatorState.pop() 方法,触发 onPopPage 回调

    navigatorKey.currentState.pop();    
复制代码

现在看起来,我们能够完美控制整个路由栈了,是不是就够了呢? 答案肯定是不够的,我们还没有处理 浏览器输入修改URL, 浏览器返回键安卓物理返回键,以及 Navigator 嵌套的问题。

Router

Navigator 2.0 的新东西,一眼看过去全是新东西。

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

RouteInformation

存在下面2种场景:

  1. RouteInformationProvider => Router , 这种情况发生在有新的路由可用,比如在浏览器中输入一个新URL,或者在代码设置初始化路由。
  2. Router => RouteInformationProvider, 这种情况只发生在通知引擎改变浏览器 URL。
class RouteInformation {

  const RouteInformation({this.location, this.state});
  /// 比如: `/`, `/path`, `/path/to/the/app`.
  final String location;

  /// 当前页面的状态,比如滚动位置,必须是可以序列化的对象.
  final Object state;
}
复制代码

RouteInformationParser

主要负责解析 RouteInformation,这里的 T 一般为 String 或者 RouteSettings,方便我们进行解析。

abstract class RouteInformationParser<T> {

  const RouteInformationParser();
  /// 浏览器中输入一个新URL,或者在代码设置初始化路由
  Future<T> parseRouteInformation(RouteInformation routeInformation);
  /// 注意如果 reportsRouteUpdateToEngine 设置为true了,这个必须实现,不能返回 null。
  /// 传入的 T 从 RouterDelegate.currentConfiguration 获得
  RouteInformation restoreRouteInformation(T configuration) => null;
}
复制代码

RouteInformationProvider

主要负责通知 RouteInformation 变化

abstract class RouteInformationProvider extends ValueListenable<RouteInformation> {
  void routerReportsNewRouteInformation(RouteInformation routeInformation) {}
}
复制代码

常用它初始化路由

routeInformationProvider: PlatformRouteInformationProvider(
  initialRouteInformation: const RouteInformation(
    location: '/mainpage',  
  ),
),
复制代码

RouterDelegate

创建和配置 Navigator 的代理,跟之前写的 Navigator demo 差不多。不同的是增加对 浏览器输入修改URL, 浏览器返回键安卓物理返回键 的处理。

abstract class RouterDelegate<T> extends Listenable {
  
  /// 初始化路由会调用该方法
  Future<void> setInitialRoutePath(T configuration) {

​    return setNewRoutePath(configuration);

  }
  
  /// 新增路由比如 浏览器中输入一个新URL,或者在代码设置初始化路由
  Future<void> setNewRoutePath(T configuration);


  /// `浏览器返回键``安卓物理返回键` 会调用该方法
  Future<bool> popRoute();
  
  /// RouteInformationParser.restoreRouteInformation 会
  /// 获取该值用于报告给引擎,特别是在 Web 应用中
  T get currentConfiguration => null;
  
  /// 返回 Navigator
  Widget build(BuildContext context);

}

复制代码

PopNavigatorRouterDelegateMixin

帮你实现了 RouterDelegate 中的 popRoute 方法,不是必须的。

mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> {
  /// The key used for retrieving the current navigator.
  ///
  /// When using this mixin, be sure to use this key to create the navigator.
  GlobalKey<NavigatorState> get navigatorKey;

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

源码分析

前面只是讲了哪些 api 解决了哪些问题,这里我们来追踪一下官方是如何实现的,我在浏览器里面输入一个新的URL。

  • 从图上最后一步,很明显看来这是一个从引擎过来的原生方法

  • 到了_handleNavigationInvocation 方法,poppush 都在这里了,这里就是接收来之引擎的通知包括 浏览器输入,浏览器和安卓物理返回按钮点击。

  Future<dynamic> _handleNavigationInvocation(MethodCall methodCall) {
    switch (methodCall.method) {
      case 'popRoute':
        return handlePopRoute();
      case 'pushRoute':
        return handlePushRoute(methodCall.arguments as String);
      case 'pushRouteInformation':
        return _handlePushRouteInformation(methodCall.arguments as Map<dynamic, dynamic>);
    }
    return Future<dynamic>.value();
  }
  
复制代码
  • 在方法里面,分别给注册过的 WidgetsBindingObserver 分发事件,当发现处理之后,return 掉。(这里预埋了嵌套 Navigator 的坑)
  Future<void> handlePushRoute(String route) async {
    for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.from(_observers)) {
      if (await observer.didPushRoute(route))
        return;
    }
  }
  
 Future<void> handlePopRoute() async {
    for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.from(_observers)) {
      if (await observer.didPopRoute())
        return;
    }
    SystemNavigator.pop();
  }
  
  Future<void> _handlePushRouteInformation(Map<dynamic, dynamic> routeArguments) async {
    for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.from(_observers)) {
      if (
        await observer.didPushRouteInformation(
          RouteInformation(
            location: routeArguments['location'] as String,
            state: routeArguments['state'] as Object,
          )
        )
      )
      return;
    }
  }  
复制代码
  • WidgetsApp 继承了 WidgetsBindingObserver,在 WidgetsBinding.instance.addObserver(this) 中把自己加入 _observers 里面
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {

  @override
  void initState() {
    WidgetsBinding.instance.addObserver(this);
  }
  
    @override
  Future<bool> didPushRoute(String route) async {
  }
复制代码
  • PlatformRouteInformationProvider 在Router 初始化的时候 addListener的时候把自己加到 _observers 中。
class _RouterState<T> extends State<Router<T>> {
  @override
  void initState() {
    super.initState();
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
复制代码
class PlatformRouteInformationProvider extends RouteInformationProvider with WidgetsBindingObserver, ChangeNotifier {
  @override
  void addListener(VoidCallback listener) {
    if (!hasListeners)
      WidgetsBinding.instance.addObserver(this);
    super.addListener(listener);
  }
复制代码

最终我们就能获取到引擎告诉我们的路由变化了!

BackButtonDispatcher

包括 RootBackButtonDispatcherChildBackButtonDispatcher。 主要是为了解决 Navigator 嵌套 ,返回按钮优先级的问题。

举个栗子:

1.MyApp 是一个 Navigator, 初始页面为 NestedMainPage,中间一个按钮点击之后 pushChildRouterPage。 现在 第一个 Navigator 中有 2个页面(NestedMainPage,ChildRouterPage)

2.ChildRouterPage 也是一个 Navigator,初始页面为 NestedTestPage,中间一个按钮点击之后 pushTestPageA。 现在 第一个 Navigator 中有 2个页面(NestedTestPage,TestPageA)

3.现在我们可以看到的是 TestPageA, 那么现在按 安卓物理返回键 或者 浏览器返回键,是什么现象?

4.页面退回到了 NestedMainPage,这一定不是大家想要的结果吧?

那么我们怎么解决这个问题呢?前面我们知道我们是有办法监听 安卓物理返回键 或者 浏览器返回键 的,也就是 _handleNavigationInvocation 方法中的 popRoute 回调,但是优先处理的第一个能够 popNavigator(还记得分发的时候那个坑吗)。实际上我们可以自己来决定 popRoute 回调作用于哪个一个 Navigator。我们只需要在第2个 Router 这里做以下操作。

获取到上一个 RouterbackButtonDispatcher,并且获取优先级。

 Widget build(BuildContext context) {
    final ChildBackButtonDispatcher childBackButtonDispatcher =
        Router.of(context)
            .backButtonDispatcher
            .createChildBackButtonDispatcher();
    childBackButtonDispatcher.takePriority();
    return Router<RouteSettings>(
      backButtonDispatcher: childBackButtonDispatcher,
    );
  }
复制代码

源码分析:

  • 很熟悉的东西 WidgetsBindingObserveraddCallback 的时候把自己加到监听 _observers ,等待引擎传递事件,WidgetsBinding 中分发。
class RootBackButtonDispatcher extends BackButtonDispatcher with WidgetsBindingObserver {
  RootBackButtonDispatcher();

  @override
  void addCallback(ValueGetter<Future<bool>> callback) {
    if (!hasCallbacks)
      WidgetsBinding.instance.addObserver(this);
    super.addCallback(callback);
  }

  @override
  void removeCallback(ValueGetter<Future<bool>> callback) {
    super.removeCallback(callback);
    if (!hasCallbacks)
      WidgetsBinding.instance.removeObserver(this);
  }

  @override
  Future<bool> didPopRoute() => invokeCallback(Future<bool>.value(false));
}
复制代码
  • 在调用 takePriority 方法时候讲自己加到 parentchildren 当中,确保自己是最后一个,并且清空了自己的 children
class ChildBackButtonDispatcher extends BackButtonDispatcher {
  ChildBackButtonDispatcher(this.parent) : assert(parent != null);
  final BackButtonDispatcher parent;
  @protected
  Future<bool> notifiedByParent(Future<bool> defaultValue) {
    return invokeCallback(defaultValue);
  }

  @override
  void takePriority() {
    parent.deferTo(this);
    super.takePriority();
  }
  
  /// BackButtonDispatcher 中的实现,方便讲解
  void deferTo(ChildBackButtonDispatcher child) {
    assert(hasCallbacks);
    _children ??= <ChildBackButtonDispatcher>{} as LinkedHashSet<ChildBackButtonDispatcher>;
    _children.remove(child); // child may or may not be in the set already
    _children.add(child);
  }
  
  /// BackButtonDispatcher 中的实现,方便讲解
  void takePriority() {
    if (_children != null)
      _children.clear();
  }
}
复制代码
  • invokeCallback 方法,从 children 最后一个开始遍历(这就是为啥在 deferTo 方法中先 remove,后 add),看谁 handle 了 didPopRoute 事件,如果处理了就停止。
  Future<bool> invokeCallback(Future<bool> defaultValue) {
    if (_children != null && _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 returns 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);
  }
复制代码

Navigator 2.0 总结

  • 通过对 Navigator.pages 的管理,实现对路由栈的完全掌握。
  • 通过 Router以及相关的 Api 解决浏览器输入修改URL, 浏览器返回键安卓物理返回键 与原生交互的问题,和对 Navigator代理和配置。
  • 通过 BackButtonDispatcher 处理了 Navigator 嵌套 的问题。

看起来,Navigator 2.0 肥肠完美了,但是实际使用也还是存在一些缺点。

  • 要实现的东西有点多,难道不能 duang 一下就能用吗?
  • Web 浏览器中手动输入参数解析的问题
  • 由于 RoutePagecreateRoute 方法中生成,导致我们没法直接访问到 Route。如果我们要写一个类似 push 有回调参数的方法该怎么办呢?
  Future<T> push<T extends Object>(Route<T> route) {
    _history.add(_RouteEntry(route, initialState: _RouteLifecycle.push));
    _flushHistoryUpdates();
    _afterNavigation(route);
    return route.popped;
  }
复制代码

嗯,是的,法法路由注解 5.0 已经完美支持 Navigator 1.02.0了,散花!

法法路由注解 5.0

增加引用

添加引用到dependencies,及你需要注解的 project/packages 到pubspec.yaml

dev_dependencies:
  ff_annotation_route_core: any
  ff_annotation_route_library: any
复制代码

执行 flutter packages get 下载

添加注解

空构造

import 'package:ff_annotation_route/ff_annotation_route.dart';

@FFRoute(
  name: "fluttercandies://mainpage",
  routeName: "MainPage",
)
class MainPage extends StatelessWidget
{
  // ...
}

复制代码

带参数构造

工具会自动处理带参数的构造,不需要做特殊处理。唯一需要注意的是,你需要使用 argumentImports 为class/enum的参数提供 import 地址。现在你可以使用 @FFArgumentImport()来替代

@FFArgumentImport('hide TestMode2')
import 'package:example1/src/model/test_model.dart';
@FFArgumentImport()
import 'package:example1/src/model/test_model1.dart' hide TestMode3;
import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';

@FFRoute(
  name: 'flutterCandies://testPageE',
  routeName: 'testPageE',
  description: 'Show how to push new page with arguments(class)',
  // 为了防止 @FFArgumentImport() 不能完全表达的情况, 依然保留 argumentImports。
  // argumentImports: <String>[
  //   'import \'package:example1/src/model/test_model.dart\';',
  //   'import \'package:example1/src/model/test_model1.dart\';',
  // ],
  exts: <String, dynamic>{
    'group': 'Complex',
    'order': 1,
  },
)
class TestPageE extends StatelessWidget {
  const TestPageE({
    this.testMode = const TestMode(
      id: 2,
      isTest: false,
    ),
    this.testMode1,
  });
  factory TestPageE.deafult() => TestPageE(
        testMode: TestMode.deafult(),
      );

  factory TestPageE.required({@required TestMode testMode}) => TestPageE(
        testMode: testMode,
      );

  final TestMode testMode;
  final TestMode1 testMode1;
}
复制代码

FFRoute

ParameterDescriptionDefault
name路由的名字 (e.g., "/settings")required
showStatusBar是否显示状态栏true
routeName用于埋点收集数据的页面名字''
pageRouteType路由的类型 (material, cupertino, transparent)-
description路由的描述''
exts其他扩展参数.-
argumentImports某些参数的导入.有一些参数是类或者枚举,需要指定它们的导入地址,现在你可以使用 @FFArgumentImport()来替代-

生成文件

环境

添加 dart 的 bin 的路径到你的系统 $PATH.

cache\dart-sdk\bin

更多信息

不清楚的可以看掘金

激活

pub global activate ff_annotation_route

执行命令

到你的项目根目录下面执行.

ff_route <command> [arguments]

命令参数

可用的命令:

-h, --[no-]help                   帮助信息。

-p, --path                        执行命令的目录,默认当前目录。

-o, --output                      route 和 helper 文件的输出目录路径,路径相对于主项目的 lib 文件夹。

-n, --name                        路由常量类的名称,默认为 `Routes`。

-g, --git                         扫描 git 引用的 package,你需要指定 package 的名字,多个用 `,` 分开
    --routes-file-output          routes 文件的输出目录路径,路径相对于主项目的lib文件夹
    --const-ignore                使用正则表达式忽略一些const(不是全部const都希望生成)
    --[no-]route-constants        是否在根项目中的 `xxx_route.dart` 生成全部路由的静态常量
    --[no-]package                这个是否是一个 package
    --[no-]supper-arguments       是否生成路由参数帮助类

-s, --[no-]save                   是否保存命令到本地。如果保存了,下一次就只需要执行 `ff_route` 就可以了。

复制代码

注解 Navigator 1.0

完整代码在 example

Main.dart

import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';
import 'package:flutter/material.dart';
import 'example_route.dart';
import 'example_routes.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ff_annotation_route demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: Routes.fluttercandiesMainpage,
      onGenerateRoute: (RouteSettings settings) {
        return onGenerateRoute(
          settings: settings,
          getRouteSettings: getRouteSettings,
          routeSettingsWrapper: (FFRouteSettings ffRouteSettings) {
            if (ffRouteSettings.name == Routes.fluttercandiesMainpage ||
                ffRouteSettings.name ==
                    Routes.fluttercandiesDemogrouppage.name) {
              return ffRouteSettings;
            }
            return ffRouteSettings.copyWith(
                widget: CommonWidget(
              child: ffRouteSettings.widget,
              title: ffRouteSettings.routeName,
            ));
          },
        );
      },
    );
  }
}
复制代码

Push

Push name
  Navigator.pushNamed(context, Routes.fluttercandiesMainpage /* fluttercandies://mainpage */);
复制代码
Push name with arguments
  • 参数必须是一个 Map<String, dynamic>
  Navigator.pushNamed(
    context,
    Routes.flutterCandiesTestPageE,
    arguments: <String, dynamic>{
      constructorName: 'required',
      'testMode': const TestMode(
        id: 100,
        isTest: true,
      ),
    },
  );
复制代码
  • 开启 --supper-arguments
  Navigator.pushNamed(
    context,
    Routes.flutterCandiesTestPageE.name,
    arguments: Routes.flutterCandiesTestPageE.requiredC(
      testMode: const TestMode(
        id: 100,
        isTest: true,
      ),
    ),
  );
复制代码

注解 Navigator 2.0

完整代码在 完整代码在 example1

Main.dart

import 'dart:convert';
import 'package:example1/src/model/test_model.dart';
import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'example1_route.dart';
import 'example1_routes.dart';

void main() {
  // 工具将处理简单的类型,但是没法处理全部的
  // 比如在浏览器中输入以下地址
  // http://localhost:64916/#flutterCandies://testPageF?list=[4,5,6]&map={"ddd":123}&testMode={"id":2,"isTest":true}
  // queryParameters 将会根据你自身的情况转换成你对应的类型
  FFConvert.convert = <T>(dynamic value) {
    if (value == null) {
      return null;
    }
    print(T);
    final dynamic output = json.decode(value.toString());
    if (<int>[] is T && output is List<dynamic>) {
      return output.map<int>((dynamic e) => asT<int>(e)).toList() as T;
    } else if (<String, String>{} is T && output is Map<dynamic, dynamic>) {
      return output.map<String, String>((dynamic key, dynamic value) =>
          MapEntry<String, String>(key.toString(), value.toString())) as T;
    } else if (const TestMode() is T && output is Map<dynamic, dynamic>) {
      return TestMode.fromJson(output) as T;
    }

    return json.decode(value.toString()) as T;
  };
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final FFRouteInformationParser _ffRouteInformationParser =
      FFRouteInformationParser();

  final FFRouterDelegate _ffRouterDelegate = FFRouterDelegate(
    getRouteSettings: getRouteSettings,
    pageWrapper: <T>(FFPage<T> ffPage) {
      return ffPage.copyWith(
        widget: ffPage.name == Routes.fluttercandiesMainpage ||
                ffPage.name == Routes.fluttercandiesDemogrouppage.name
            ? ffPage.widget
            : CommonWidget(
                child: ffPage.widget,
                routeName: ffPage.routeName,
              ),
      );
    },
  );
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'ff_annotation_route demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // 初始化第一个页面
      routeInformationProvider: PlatformRouteInformationProvider(
        initialRouteInformation: const RouteInformation(
          location: Routes.fluttercandiesMainpage,
        ),
      ),
      routeInformationParser: _ffRouteInformationParser,
      routerDelegate: _ffRouterDelegate,
    );
  }
}
复制代码

FFRouteInformationParser

主要用在 Web 平台,当你在浏览器上面输入的时候路由配置转换成为 RouteSettings ,或者当反馈给浏览器的时候将 RouteSettings 转换成路由配置

举个例子:

xxx?a=1&b=2 <=> RouteSettings(name:'xxx',arguments:<String, dynamic>{'a':'1','b':'2'})

FFRouterDelegate

用于创建和配置导航的委托,它提供 Navigator 中相似的方法.

  FFRouterDelegate.of(context).pushNamed<void>(
    Routes.flutterCandiesTestPageF.name,
    arguments: Routes.flutterCandiesTestPageF.d(
      <int>[1, 2, 3],
      map: <String, String>{'ddd': 'dddd'},
      testMode: const TestMode(id: 1, isTest: true),
    ),
  );
复制代码

你可以在 test_page_c.dart 页面里面找到更多的例子

Push

Push name
  FFRouterDelegate.of(context).pushNamed<void>(
    Routes.flutterCandiesTestPageA,
  );
复制代码
Push name with arguments
  • 参数必须是一个 Map<String, dynamic>
  FFRouterDelegate.of(context).pushNamed<void>(
    Routes.flutterCandiesTestPageF.name,
    arguments: Routes.flutterCandiesTestPageF.d(
      <int>[1, 2, 3],
      map: <String, String>{'ddd': 'dddd'},
      testMode: const TestMode(id: 1, isTest: true),
    ),
  );
复制代码
  • 开启 --supper-arguments
  FFRouterDelegate.of(context).pushNamed<void>(
    Routes.flutterCandiesTestPageF.name,
    arguments: <String, dynamic>{
        'list': <int>[1, 2, 3],
        'map': <String, String>{'ddd': 'dddd'},
        'testMode': const TestMode(id: 1, isTest: true),
     }
  )
复制代码

Code Hints

你能这样使用路由 'Routes.flutterCandiesTestPageE', 并且在编辑器中看到代码提示。 包括页面描述,构造,参数类型,参数名字,参数是否必填。

  • 默认
  /// 'This is test page E.'
  ///
  /// [name] : 'flutterCandies://testPageE'
  ///
  /// [routeName] : 'testPageE'
  ///
  /// [description] : 'This is test page E.'
  ///
  /// [constructors] :
  ///
  /// TestPageE : [TestMode testMode, TestMode1 testMode1]
  ///
  /// TestPageE.deafult : []
  ///
  /// TestPageE.required : [TestMode(required) testMode]
  ///
  /// [exts] : {group: Complex, order: 1}
  static const String flutterCandiesTestPageE = 'flutterCandies://testPageE';
复制代码
  • 开启 --supper-arguments
  /// 'This is test page E.'
  ///
  /// [name] : 'flutterCandies://testPageE'
  ///
  /// [routeName] : 'testPageE'
  ///
  /// [description] : 'This is test page E.'
  ///
  /// [constructors] :
  ///
  /// TestPageE : [TestMode testMode, TestMode1 testMode1]
  ///
  /// TestPageE.test : []
  ///
  /// TestPageE.requiredC : [TestMode(required) testMode]
  ///
  /// [exts] : {group: Complex, order: 1}
  static const _FlutterCandiesTestPageE flutterCandiesTestPageE =
      _FlutterCandiesTestPageE();

  class _FlutterCandiesTestPageE {
    const _FlutterCandiesTestPageE();

    String get name => 'flutterCandies://testPageE';

    Map<String, dynamic> d(
            {TestMode testMode = const TestMode(id: 2, isTest: false),
            TestMode1 testMode1}) =>
        <String, dynamic>{
          'testMode': testMode,
          'testMode1': testMode1,
        };

    Map<String, dynamic> test() => const <String, dynamic>{
          'constructorName': 'test',
        };

    Map<String, dynamic> requiredC({@required TestMode testMode}) =>
        <String, dynamic>{
          'testMode': testMode,
          'constructorName': 'requiredC',
        };

    @override
    String toString() => name;
  }

复制代码

结语

  • 路由我定,人生中也有很多的十字路,以及选择需要我们去定。选择大于努力,有时候以为只有一条路,其实也可以有另外的选择。有时候以为还有其他的选择,其实只剩下一条路。站在人生十字路口希望我们都不再彷徨,不管是现在的我,还是将来的你,都不希望会后知后觉。
  • Flutter 也两年了,从开始孤自一人研究到现在有很多小伙伴可以讨论, Flutter 让我学习到了很多东西,也认识了更多的人。不知道程序员能写多久的代码,但从重庆轻轨做通信项目到转行来写代码,是真的因为喜欢。我想一个人能做自己喜欢的事情,应该也是一种幸福吧。
  • 很感谢你能读到这里,希望本文对你有所帮助。爱Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果QQ群:181398081。最最后放上Flutter Candies全家桶,真香。