【Flutter开发】Navigator2.0介绍及使用

news/2024/7/20 20:23:48 标签: android, flutter, ios

在这里插入图片描述

目录

  • Navigator1.0
  • Navigator2.0
    • APP
    • RouteInformationParser
    • RouterDelegate
  • 问题
    • The Navigator.pages must not be empty to use the Navigator.pages API
    • 浏览器的回退按钮
  • 总结

Navigator1.0

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

class MyApp extends StatelessWidget {
  
  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。

APP

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

class MyApp extends StatelessWidget {
  final delegate = MyRouteDelegate();

  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      routerDelegate: delegate,
      routeInformationParser: MyRouteParser(),
    );
  }
}

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

RouteInformationParser

创建一个类继承RouteInformationParser,主要的作用是包装解析路由信息,这里有一个最简单的方式,如下:

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

  
  RouteInformation restoreRouteInformation(String configuration) {
    return RouteInformation(location: configuration);
  }
}

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

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{

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

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

  final _stack = <String>[];

  
  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();
    }
  }

  
  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的代码:

  
  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函数,如下:

  
  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方法,如下:

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

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

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

问题

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

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();

  
  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中才创建就会出现上面的问题,如果像上面代码一样在初始化创建就没有这个问题了。

浏览器的回退按钮

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

这个问题有个官方issues:https://github.com/flutter/flutter/issues/71122

其中官方提到:

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,比较复杂,所以目前这个问题并没有很好的解决方法。


有关浏览器后退的问题,可以看我另外一篇博客《Flutter Web中刷新与后退问题》

总结

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


http://www.niftyadmin.cn/n/336365.html

相关文章

usb摄像头驱动打印信息

usb摄像头驱动打印信息 文章目录 usb摄像头驱动打印信息 在ubuntu中接入罗技c920摄像头打印的信息如下&#xff1a; [ 100.873222] usb 3-2: new high-speed USB device number 5 using xhci_hcd [ 101.230728] usb 3-2: New USB device found, idVendor046d, idProduct08e5 …

VMware Workstation 与 Device/Credential Guard 不兼容.在禁用 Device/Credenti

这个时候我们需要去关掉几个功能 1、关闭Hyper-V 打开控制面板首页&#xff0c;找到“程序”&#xff0c;然后找到“启用或关闭Windows功能”&#xff0c;找到“Hyper-V”&#xff0c;有勾中的全部都取消掉&#xff0c;如果这一步操作失败&#xff0c;不要紧&#xff0c;继续…

svn清理以下路径失败显示乱码问题

报错&#xff1a; svn作为我们经常使用的版本管理服务器&#xff0c;在使用过程中经常需要通过clean up操作来完成本地文件与服务器文件信息及版本信息同步&#xff0c; 然而有时右键会在执行清理命令时提示“清理以下路径失败&#xff1a; xxx 文件名、目录名或卷标语法不…

P1344 [USACO4.4] 追查坏牛奶 Pollutant Control (网络流)

P1344 [USACO4.4] 追查坏牛奶 Pollutant Control &#xff08;网络流&#xff09; 题目链接 文章目录 P1344 [USACO4.4] 追查坏牛奶 Pollutant Control &#xff08;网络流&#xff09;题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示题目大意思路分析code 双倍经…

【硬核】C语言指针是什么?深入浅出带你掌握C语言指针!

指针与底层硬件联系紧密&#xff0c;使用指针可操作数据的地址&#xff0c;实现数据的间接访问&#xff0c;本文章内容如下 1、C语言指针的作用 2、计算机的存储机制 3、如何定义指针 4、如何操作指针 5、数组与指针的关系 6、指针使用中的一些注意事项 1、C语言指针有什么作用…

MapReduce【自定义分区Partitioner】

实际开发中我们可能根据需求需要将MapReduce的运行结果生成多个不同的文件&#xff0c;比如上一个案例【MapReduce计算广州2022年每月最高温度】&#xff0c;我们需要将前半年和后半年的数据分开写到两个文件中。 默认分区 默认MapReduce只能写出一个文件&#xff1a; 因为我…

看模型、做技术交底、做项目汇报,图新说数字化汇报平台引领交互式汇报新模式

现场汇报效果不好&#xff0c;导致丢了一个项目&#xff01; 项目汇报平淡无奇&#xff0c;方案屡次被毙&#xff01; 面对专家质疑&#xff0c;回答苍白无力&#xff01; 估计大家都有过这种经历和感受。 详细分析一下&#xff0c;基本上有以下几个方面的原因&#xff1a; …

腾讯云轻量应用服务器卡死怎么连接?

腾讯云轻量云服务器卡死怎么解决&#xff1f;使用腾讯云自带的VNC登录连接轻量服务器&#xff0c;或使用腾讯云OrcaTerm一键免密登录轻量实例。如果是确定数据没问题&#xff0c;也可以使用控制台自带的重启实例。 腾讯云轻量应用服务器参考&#xff1a;https://curl.qcloud.co…