IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Flutter:Navigator2.0介绍及使用 -> 正文阅读

[移动开发]Flutter: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 {
  @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(),
    );
  }
}

通过onGenerateRoute或routes来注册路由,使用时通过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();

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

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

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的形式,这样方便处理。

RouterDelegate

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

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

其中addListener和removeListener是来自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,这样就不必实现addListener和removeListener了。

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

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

通过上面两个继承,我们只需要实现setNewRoutePath和build两个函数即可。先看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,设置pages和onPopPage。

在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。

问题

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

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                      <fn>
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                      <fn>
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                      <fn>
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                        <fn>
...
════════════════════════════════════════════════════════════════════════════════════════════════════

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

浏览器的回退按钮

经过测试发现,浏览器的后退按钮点击后并不执行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,比较复杂,所以目前这个问题并没有很好的解决方法。

总结

?

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-07 12:12:20  更:2021-08-07 12:14:00 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 11:49:51-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码