Flutter:Navigator2.0介绍及使用

简介: 我们学习flutter一开始接触的路由管理就是Navigator1.0,它非常方便,使用简单

Navigator1.0


我们学习flutter一开始接触的路由管理就是Navigator1.0,它非常方便,使用简单,如下:


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      onGenerateRoute: (RouteSettings settings){
        return PageRouteBuilder(
          settings: settings,
          pageBuilder: (BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation) {
            if(settings.name == "pageB"){
              return PageB();
            }
            else if(settings.name == "pageC"){
              return PageC();
            }
            else{
              return Container();
            }
          }
        );
      },
      // routes: {
      //   "pageB" : (BuildContext context) => PageB(),
      //   "pageC" : (BuildContext context) => PageC()
      // },
      home: PageA(),
    );
  }
}
复制代码


通过onGenerateRouteroutes来注册路由,使用时通过Navigator.of(context).pushNamed()或者其他函数即可。

Navigator1.0使用简单,但是问题也一样,只有push、pop等几个简单操作,对于复杂场景就无能为力了,比如web开发时地址栏或后退键的处理。

所以google后来又推出了Navigator2.0


Navigator2.0


Navigator1.0是通过Navigator来管理处理路由,而Navigator2.0则是通过Router来处理的,但是也需要Navigator,实际上是用Router对Navigator包裹起来。Router相对来说功能就强大很多了,同时使用起来也复杂很多。

关于Navigator2.0的原理,网上已经有很多文章了,但是我发现这些文章在使用实例上都不是很清楚,或者说示例过于复杂。应该是大部分参考google官方文档简单翻译的,但是其实我们正常场景使用并不是那么复杂,而且大部分都没有讲清楚。所以本篇文章不讨论原理,只用最简单的示例来展示如果使用Navigator2.0,或者说如何快速的从Navigator1.0转成Navigator2.0。


1)APP


首先创建MaterialApp方式有了改变,通过MaterialApp.router()来创建,如下:

class MyApp extends StatelessWidget {
  final delegate = MyRouteDelegate();
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      routerDelegate: delegate,
      routeInformationParser: MyRouteParser(),
    );
  }
}
复制代码


通过这种方式我们需要设置routerDelegaterouteInformationParser,这样就需要实现这两个类。


2)RouteInformationParser


创建一个类继承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);
  }
}
复制代码


我们的路由信息都由一个字符串承载,可以用url的形式,这样方便处理。


3)RouterDelegate


RouterDelegate是最重要的部分,这里实现路由切换的逻辑,继承RouterDelegate的类需要实现下面的函数:


void addListener(listener) 
  void removeListener(listener)
  Widget build(BuildContext context)
  Future<bool> popRoute() 
  Future<void> setNewRoutePath(T configuration)
复制代码


其中addListenerremoveListener是来自RouterDelegate的继承Listenable。

build一般返回的是一个Navigator。

popRoute实现后退逻辑

setNewRoutePath实现新页面的逻辑

单单这么说肯定一头雾水,我们用一个示例来实现它,具体代码如下:


class MyRouteDelegate extends RouterDelegate<String> with PopNavigatorRouterDelegateMixin<String>, ChangeNotifier{
  @override
  GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
  @override
  String get currentConfiguration => _stack.isNotEmpty ? _stack.last : null;
  final _stack = <String>[];
  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        for (final url in _stack)
          getPage(url)
      ],
      onPopPage: (route, result){
        if (_stack.isNotEmpty) {
          _stack.removeLast();
          notifyListeners();
        }
        return route.didPop(result);
      },
    );
  }
  Page getPage(String url){
    return MaterialPage(
        name: url,
        arguments: null,
        child: getWidget(url)
    );
  }
  Widget getWidget(String name){
    switch(name){
      case "pageB":
        return PageB();
      case "pageC":
        return PageC();
      default:
        return PageA();
    }
  }
  @override
  Future<void> setNewRoutePath(String config) {
    if(config == "/"){
      _stack.clear();
    }
    if(_stack.isEmpty || config != _stack.last) {
      _stack.add(config);
      notifyListeners();
    }
    return SynchronousFuture<void>(null);
  }
}
复制代码


首先我们不仅继承RouterDelegate,同时还继承ChangeNotifier,这样就不必实现addListenerremoveListener了。

注意:如果这里手动实现了addListenerremoveListener但是并没有实现代码,这样会导致页面无法切换,因为路由变化没有通知。现象就是点击切换页面的按钮无反应,build不执行。

然后又继承了PopNavigatorRouterDelegateMixin,它实现了popRoute函数,所以这个函数也可以不用实现。但是继承它后需要实现navigatorKey,如上第一行。

通过上面两个继承,我们只需要实现setNewRoutePathbuild两个函数即可。先看setNewRoutePath的代码:


@override
  Future<void> setNewRoutePath(String config) {
    if(config == "/"){
      _stack.clear();
    }
    if(_stack.isEmpty || config != _stack.last) {
      _stack.add(config);
      notifyListeners();
    }
    return SynchronousFuture<void>(null);
  }
复制代码


_stack是一个列表,用来存储所有路由信息,因为前面我们的路由信息用String承载,所以_stack是一个字符串列表。

在这个函数里将新路由添加进_stack,然后调用notifyListeners()通知路由变化。

注意这里的两个逻辑,如果是首页则先清空;如果新页面与上一页一摸一样,则忽略,因为发现在web上setNewRoutePath会被重复调用。

然后是build函数,如下:


@override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        for (final url in _stack)
          getPage(url)
      ],
      onPopPage: (route, result){
        if (_stack.isNotEmpty) {
          _stack.removeLast();
          notifyListeners();
        }
        return route.didPop(result);
      },
    );
  }
复制代码


返回一个Navigator,设置pagesonPopPage

onPopPage中实现回退逻辑,可以看到将列表中最后一个remove掉,然后notifyListeners()同时路由变化。上面我们提到PopNavigatorRouterDelegateMixin实现了popRoute函数,它的实现代码最终就会调用到onPopPage这里。

pages则是一个Page列表,是当前已经打开的所有页面,所以用一个for循环来创建,我自己定义了一个getPage函数:


Page getPage(String url){
    return MaterialPage(
        name: url,
        arguments: null,
        child: getWidget(url)
    );
  }
  Widget getWidget(String name){
    switch(name){
      case "pageB":
        return PageB();
      case "pageC":
        return PageC();
      default:
        return PageA();
    }
  }
复制代码


注意:因为我们的示例中路由没有参数,只有路由名称,所以上面对url没有进行处理。但是实际使用的时候,在getPage函数一开始就应该对url进行处理,提取出name和参数,并将参数整理成Object设置给arguments,这样页面中就可以用之前的方式ModalRoute.of(context).settings.arguments获取,不用改变太多。

这里我定义了三个页面,其中PageA是默认页面。三个页面都很简单,每个页面有两个按钮,一个打开新页面,一个回退。

打开新页面用


Router.of(context).routerDelegate.setNewRoutePath("pageB");
复制代码


代替了之前Navigator1.0中的


Navigator.of(context).pushNamed("pageB");
复制代码


回退则使用


Router.of(context).routerDelegate.popRoute();
复制代码


代替了之前Navigator1.0中的


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


这样页面内的改动很小,可以很快的转到Navigator2.0。

到这里还差最后一步,实现RouterDelegate中字段currentConfiguration的get方法,如下:


@override
  String get currentConfiguration => _stack.isNotEmpty ? _stack.last : null;
复制代码


如果不实现这里,虽然页面可以切换,但是路由信息并没有更新,比如flutter web的应用在浏览器中,页面正常切换,但是地址栏并没有变化。只有实现了这个get函数,当路由发生变化的时候,其他类才能通过这个函数获取到最新路由。

上面就是Navigator2.0的简单使用,相对于官方的示例更简单一些,也更容易理解核心部分,尤其方便从Navigator1.0升级到Navigator2.0。


问题


这个过程还是出现不少问题的,记录一下:


1)The Navigator.pages must not be empty to use the Navigator.pages API

报错如下:

════════ Exception caught by widget library ════════════════════════════════════════════════════════ The following assertion was thrown: The Navigator.pages must not be empty to use the Navigator.pages API

When the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 906:28       get current packages/flutter/src/widgets/navigator.dart 3345:33                       packages/flutter/src/widgets/navigator.dart 3361:14                      initState packages/flutter/src/widgets/framework.dart 4632:57                      [_firstBuild] packages/flutter/src/widgets/framework.dart 4469:5                       mount ... ════════════════════════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by widgets library ═══════════════════════════════════════════════════════ Navigator.onGenerateRoute was null, but the route named "/" was referenced. The relevant error-causing widget was: MaterialApp file:///Users/bennu/fluttertest/lib/main.dart:62:24 ════════════════════════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by widget library ════════════════════════════════════════════════════════ The following assertion was thrown: The Navigator.pages must not be empty to use the Navigator.pages API

When the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 906:28       get current packages/flutter/src/widgets/navigator.dart 3345:33                       packages/flutter/src/widgets/navigator.dart 3361:14                      initState packages/flutter/src/widgets/framework.dart 4632:57                      [_firstBuild] packages/flutter/src/widgets/framework.dart 4469:5                       mount ... ════════════════════════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by widgets library ═══════════════════════════════════════════════════════ Navigator.onGenerateRoute was null, but the route named "/" was referenced. The relevant error-causing widget was: MaterialApp file:///Users/bennu/fluttertest/lib/main.dart:62:24 ════════════════════════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by widget library ════════════════════════════════════════════════════════ The following assertion was thrown: A HeroController can not be shared by multiple Navigators. The Navigators that share the same HeroController are:

  • NavigatorState#1f365(lifecycle state: initialized)
  • NavigatorState#9f699(lifecycle state: initialized)

Please create a HeroControllerScope for each Navigator or use a HeroControllerScope.none to prevent subtree from receiving a HeroController. When the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 906:28       get current packages/flutter/src/widgets/navigator.dart 3501:41                       packages/flutter/src/scheduler/binding.dart 1144:15                      [_invokeFrameCallback] packages/flutter/src/scheduler/binding.dart 1090:9                       handleDrawFrame packages/flutter/src/scheduler/binding.dart 865:7                         ... ════════════════════════════════════════════════════════════════════════════════════════════════════


这里涉及到一开始App的创建,回顾一下代码:


class MyApp extends StatelessWidget {
  final delegate = MyRouteDelegate();
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      routerDelegate: delegate,
      routeInformationParser: MyRouteParser(),
    );
  }
}
复制代码


注意MyRouteDelegate并不是在build中创建的,而是在初始化时就创建了。如果在build中才创建就会出现上面的问题,如果像上面代码一样在初始化创建就没有这个问题了。


2)浏览器的回退按钮



经过测试发现,浏览器的后退按钮点击后并不执行pop操作,而是执行setNewRoutePath,这样就会导致回退的时候实际上_stack并没有移除当前页面,反而将上一个页面重新添加进来了,这样_stack路径就乱了。

这个问题有个官方issues:github.com/flutter/flu…

其中官方提到:

the browser backward button no longer tie to the didpopRoute in navigator 2.0. it is now acting as deeplinking. Whenever backward or forward button is pressed, the web engine will get the new url and send that to the framework through didpushRoute.

BackButtonDispatcher is for android back button, it will only be triggered in android.

这里涉及的BackButtonDispatcher也是Navigator2.0的功能,可以拦截处理返回键,但是通过上面可以看出这个功能只对android的返回键有效。而在web上,无论是前进还是后退键,都是当初新的url处理,会执行didpushRoute,所以就执行到了setNewRoutePath,而不是pop。


issues中也提到了,目前官方没有解决这个问题,不过已经列入todo列表了,目前想要解决这个问题需要我们自己手动开发一个plugin,可能需要在native层处理,即在html中通过history处理并暴露api给flutter,比较复杂,所以目前这个问题并没有很好的解决方法。


总结


通过上面可以看出,Navigator2.0相对来说复杂很多,开发和学习成本大大提高,这也是很多人诟病的原因,所以有人认为Navigator2.0是一个失败的改造,这也导致目前大家很少使用它。


目录
相关文章
|
1月前
Flutter中的OverflowBox、SizedOverflowBox,详细介绍
Flutter中的OverflowBox、SizedOverflowBox,详细介绍 在Flutter中,当一个widget的大小超出了其父widget的大小时,通常会发生溢出现象。为了解决这个问题,Flutter提供了两个widget:OverflowBox和SizedOverflowBox。
125 0
|
8月前
|
Dart 开发工具 Android开发
Flutter
Flutter 是 Google 开发的一款开源 UI 工具包,它可以帮助开发者使用一套代码库快速构建美观且高性能的 Android 和 iOS 应用程序。Flutter 具有热重载(Hot Reload)和快速应用程序开发(Rapid Application Development)的特点,使得开发过程更加高效。
122 6
|
9月前
Flutter 问题系列
Flutter 问题系列
27 0
flutter系列之:使用SliverList和SliverGird
在上一篇文章我们讲解SliverAppBar的时候有提到过,Sliver的组件一般都用在CustomScrollView中。除了SliverAppBar之外,我们还可以为CustomScrollView添加List或者Grid来实现更加复杂的组合效果。 今天要向大家介绍的就是SliverList和SliverGird。
flutter系列之:使用SliverList和SliverGird
|
Dart 前端开发 JavaScript
Flutter快速了解
Flutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件。自从2017年5月发布第一个版本以来,目前Flutter已经发布了近60个版本,并且在2018年5月发布了第一个“Ready for Production Apps”的Beta 3版本,6月20日发布了第一个“Release Preview”版本。
使用 Flutter LinearGradient
使用 Flutter LinearGradient
390 0
使用 Flutter LinearGradient
|
容器
Flutter | Sliver 系列
Flutter | Sliver 系列
Flutter之 ImageWidger
Flutter之 ImageWidger
165 0
Flutter之 ImageWidger