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

news2024/11/20 4:17:50

在这里插入图片描述

目录

  • 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.coloradmin.cn/o/541238.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JAVA-Activiti 7与达梦、人大金仓兼容-nacos、服务pom文件配置(2)

目录 第一步,修改nacos服务配置 >需注意< 第二步,pom.xml依赖包配置 Activiti的源码包解决之后,接下来就好做很多了 第一步,修改nacos服务配置 spring:datasource:url: jdbc:kingbase8://127.0.0.1:54321/progress?currentSchemaprogress,productNamePostgreSQL,SYS…

保密+完整+可用+安全,规避代码安全「马奇诺防线」,构建软件供应链整体安全

近日&#xff0c;在「江狐会」广州站上&#xff0c;极狐(GitLab) 高级解决方案架构师武让分享了如何通过三大阶段 四大要点&#xff0c;规避代码安全「马奇诺防线」&#xff0c;真正确保软件供应链安全。以下内容整理自本次演讲。Enjoy&#xff5e; 先跟大家分享一个故事 一战…

计算机体系结构|MIT6.175和MIT6.375学习笔记

在2023年初&#xff0c;达坦科技发起成立硬件设计学习社区&#xff0c;邀请所有有志于从事数字芯片设计的同学加入我们的学习互助自学小组&#xff0c;以理解数字芯片设计的精髓&#xff0c;强化理论知识的同时提升实操技能&#xff0c;继而整体提升设计能力。现在&#xff0c;…

Vmware虚拟机安装MacOS13-Ventura详细教程

小编亲测 前提准备 功能强大的 Windows 电脑&#xff08;不能太差&#xff0c;不然会卡&#xff09;至少8GB内存默认是80GB的存储空间VMWare Workstation&#xff08;版本应该没什么需求&#xff0c;我装的是VMware Workstation 17 Pro&#xff09;Unlocker解锁软件MacOS Ventu…

最快的 Houdini 和 V-Ray 云渲染服务

Houdini是SideFX开发的一款3D动画软件应用。Houdini 最常用于 FX 部门&#xff0c;用于在电影和游戏中创建视觉效果。它被主要的 VFX 公司使用&#xff0c;例如 Walt Disney Animation Studios、Pixar、DreamWorks Animation、Double Negative、ILM、MPC、Framestore 等。Houdi…

推荐几款音频转文字软件给你

不知道小伙伴们有没有遇到过这种情况&#xff0c;在上学时期&#xff0c;我们经常需要记录老师上课的板书内容&#xff0c;但是边听边记可能速度会跟不上&#xff0c;还会遗漏掉一些内容&#xff0c;而且效率也不高。其实这时候&#xff0c;我们可以将老师讲话的内容先录制下来…

鸿蒙Hi3861学习十三-Huawei LiteOS-M(STA模式)

一、简介 AP&#xff08;Access Point&#xff09;无线接入点 AP是无线接入点&#xff0c;是一个无线网络的创建者&#xff0c;是网络的中心节点。一般家庭或办公室使用的无线路由器就是一个AP。 STA&#xff08;Station&#xff09;站点 STA也可以理解为终端的意思&#xff…

高性能零售IT系统的建设10-一个系统日志记录搞崩了整个公司的O2O交易系统

背景 绝大多数业务系统其实都是一座屎山&#xff0c;本人接手的这座屎山目前已经成了一座金山。这其中的幸酸只有那些从0参与过并活到现在的一些“老人”们心中自知其中的滋味。 在3年半前&#xff0c;本以为买来的一套将近600万行代码、达800张表、几乎用到了所有的互联网中间…

分布式系统:高并发

目录 1.什么是高并发 2.术语 3.如何应对处理高并发 3.1.提升系统的并发能力 3.3.1.垂直扩展 3.3.2.水平扩展 3.2.流量控制 4.削峰 4.1.怎样来实现流量削峰方案 4.2.限流 5.总结 1.什么是高并发 高并发是指系统在同一时间内处理大量请求的能力。在软件开发中&a…

CRM管理系统在线用

一、CRM管理系统是什么 CRM是客户关系管理的缩写&#xff0c;是指企业通过建立客户档案、跟进客户需求、提供优质服务来维系客户关系的一种管理模式。是企业以客户关系为重点&#xff0c;通过开展系统化的客户研究&#xff0c;优化企业组织体系和业务流程&#xff0c;提高客户…

Jina AI全新Inference服务,LangChain开发体验从未如此丝滑

由于 Token 的限制&#xff0c;在开发 LangChain 问答机器人应用时&#xff0c;我们经常需要将文档切割&#xff0c;接着使用 Embedding 引擎 分别将分割后的 Document 变成 Embeddings&#xff0c;即向量表示。 同时输入的问题&#xff0c;也需要用到 Embedding 引擎 变成向量…

linux调试知识:手把手教你SSH怎么链接

在机器装机后&#xff0c;如果没有显示&#xff0c;有没有串口&#xff0c;通常很难区操作调试&#xff0c;本文总结一篇通过搭建SSH链接去为调试做服务&#xff1a; 首先第一步&#xff1a;安装必要的软件&#xff0c;CRT或者XSHELL。 下面将举实际案例&#xff0c;手把手搭…

Axure rp9 引入Echarts图表 |手动引入图表 Apache Echarts

Axure rp9 引入Echarts图表 |手动引入图表 Apache Echarts 1.拖入一个矩形lable&#xff0c;调整合适大小,并命名为test 2.给test新建交互载入时&#xff0c;打开链接&#xff0c;并将下方code贴入 如果想在无网的情况下运行&#xff0c;需要在axure软件的安装目录DefaultSet…

深度学习笔记之递归网络(三)递归神经网络

深度学习笔记之递归网络——递归神经网络 引言回顾&#xff1a;潜变量自回归模型递归神经网络思想困惑度 引言 上一节介绍了基于统计算法的语言模型。本节将介绍基于神经网络的序列模型——递归神经网络。 回顾&#xff1a;潜变量自回归模型 关于潜变量自回归模型&#xff0…

记一次 Visual Studio 2022 卡死分析

一&#xff1a;背景 1. 讲故事 最近不知道咋了&#xff0c;各种程序有问题都寻上我了&#xff0c;你说 .NET 程序有问题找我能理解&#xff0c;Windows 崩溃找我&#xff0c;我也可以试试看&#xff0c;毕竟对 Windows 内核也知道一丢丢&#xff0c;那 Visual Studio 有问题找…

揭秘市场热销的4款问卷调查工具

当谈到进行在线问卷调查时&#xff0c;选择正确的工具可以使调查过程完全不同。市场上有这么多可供选择的产品&#xff0c;要找到一款符合我们需求的工具不是一件容易的事儿。在本文中&#xff0c;小编将和大家一起讨论4款市面上好用的问卷调查工具盘点&#xff0c;并比较它们的…

基于C语言设计一个叫号系统

访问【WRITE-BUG数字空间】_[内附完整源码和文档] 这道题的重点在于怎么处理患者的治疗过程。大二上学期的理论课上&#xff0c;我们在第一节的研讨课上对于这道题的实现进行了探讨。本题的患者排队与数据结构中的队列结构完全符合&#xff0c;当患者挂号后&#xff0c;检查该…

语音工牌:从线下沟通过程入手,实现运营商上门安装流程监管

近年来&#xff0c;随着网络的飞速发展&#xff0c;宽带越来越成为人们生活中必不可少的一部分&#xff0c;相应的&#xff0c;宽带上门安装、迁机及检修服务也成为运营商业务场景里重要的一环。 随着业务需求的增加和上门服务工程师队伍的壮大&#xff0c;以及消费者对服务质…

印度也开始自研 CPU ,5nm工艺、功耗是i9好几倍

前两天的新闻估计大家都看了&#xff0c;国内又一个科技巨头公司终止「造芯」。 OPPO 子公司哲库从成立到解散用了4年时间&#xff0c;这期间做出的马里亚纳X影像芯片也小有名气。 显然其目标不只是影像这一点&#xff0c;今年年初就有消息称 OPPO 自研 Soc 已经快到流片&…

Go语言中sync.Cond、atomic原子性和sync.Once的用法

目录 【sync.Cond】 【atomic原子性】 【sync.Once】 使用sync.Once实现单例模式 在 上一篇文章 中分析了Go语言sync 包中 sync.Mutex、sync.RWMutex和sync.WaitGroup的用法&#xff0c;这篇文章继续来讨论下sync包中关于 sync.Cond 、atomic原子性 和 sync.Once 的用法。…