300字范文,内容丰富有趣,生活中的好帮手!
300字范文 > Flutter:Navigator2.0介绍及使用

Flutter:Navigator2.0介绍及使用

时间:2022-03-26 07:39:26

相关推荐

Flutter:Navigator2.0介绍及使用

目录

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 {@overrideWidget 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();@overrideWidget 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> {@overrideFuture<String> parseRouteInformation(RouteInformation routeInformation) {return SynchronousFuture(routeInformation.location);}@overrideRouteInformation 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{@overrideGlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();@overrideString get currentConfiguration => _stack.isNotEmpty ? _stack.last : null;final _stack = <String>[];@overrideWidget 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();}}@overrideFuture<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的代码:

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

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

@overrideString 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 APIWhen the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 906:28 get currentpackages/flutter/src/widgets/navigator.dart 3345:33 <fn>packages/flutter/src/widgets/navigator.dart 3361:14 initStatepackages/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 APIWhen the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 906:28 get currentpackages/flutter/src/widgets/navigator.dart 3345:33 <fn>packages/flutter/src/widgets/navigator.dart 3361:14 initStatepackages/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 currentpackages/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 handleDrawFramepackages/flutter/src/scheduler/binding.dart 865:7 <fn>...════════════════════════════════════════════════════════════════════════════════════════════════════

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

class MyApp extends StatelessWidget {final delegate = MyRouteDelegate();@overrideWidget 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:/flutter/flutter/issues/71122

其中官方提到:

thebrowserbackwardbuttonnolongertietothedidpopRouteinnavigator2.0.itisnowactingasdeeplinking.Wheneverbackwardorforwardbuttonispressed,thewebenginewillgetthenewurlandsendthattotheframeworkthroughdidpushRoute.

BackButtonDispatcherisforandroidbackbutton,itwillonlybetriggeredinandroid.

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

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

总结

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

源码

关注公众号:BennuCTech,发送“Navi2”获取源码。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。