To prepare
Read our previous article about the principles of Flutter routingSource Code analysis of Flutter RoutingNavigator2.0.
To demonstrate the use of Navigator2.0, a simple example is prepared here for project download.
The nav_demo directory is an example using Navigator1.0 with four pages: splash, login, home, and Details
The code structure is as follows:
Core code:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'the Navigator 1.0',
routes: <String, WidgetBuilder> {
'/login': (_) = >const Login(),
'/home': (_) = >const Home(),
},
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: constSplash(), ); }}Copy the code
Navigator1.0 has several problems:
-
Static routes are not as flexible as dynamic routes because they do not easily pass parameters to constructors. If the third-party routing framework is not considered, static and dynamic routes are often used together. Click on the grid TAB to pull up the details page
GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute( builder: (ctx) => Details( _movieList! [i].name, _movieList! [i].imgUrl))); }, child: Column( mainAxisSize: MainAxisSize.min, children: [ Flexible(child: Image.network(_movieList![i].imgUrl)), Text(_movieList![i].name), ], ), );Copy the code
-
Difficult to deal with the Web version, address bar URL routing navigation requirements. As shown in the figure below, route is switched and the address bar remains unchanged
-
The control of routing is very inflexible, for example, the scene requirements of routing nesting; Handle Android return key requirements, etc
-
Different programming styles. Navigator 1.0 is an imperative programming paradigm, while Flutter itself is a declarative programming paradigm. Navigator 2.0 is a return to the declarative paradigm, with more of a Flutter flavor.
Refactoring using Navigator 2.0
Here, I’ll start by refactoring in a minimally modified, simplest way to use Navigator 2.0
Modify the code structure:
Add only the Router folder and the delegate.dart file. In which the custom class MyRouterDelegate inherited from RouterDelegate, and mixed with ChangeNotifier and PopNavigatorRouterDelegateMixin.
There are three methods that must be implemented:
class MyRouterDelegate extends RouterDelegate<List<RouteSettings>> with ChangeNotifier.PopNavigatorRouterDelegateMixin<List<RouteSettings>> {
final List<Page> _pages = [];
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: List.of(_pages),
onPopPage: _onPopPage,
);
}
@override
Future<void> setNewRoutePath(List<RouteSettings> configuration) async {}
/// ........................... Omit some code ………………
}
Copy the code
The setNewRoutePath method can be left blank, focusing on the build method. We created Navigator as the manager of the route and set two main parameters: Pages and onPopPage. Pages is a list of Page objects. When a route is popped, the onPopPage is called back and the developer can handle the logic of the route being pushed back.
What we really need to care about here is,Page
What is an object list?
We know that in Flutter, the word route is used to refer to pages in App, and the routing stack is the page stack. The Page class, introduced in 2.0, is essentially a route description file. This idea is similar to the four trees of A Flutter that I mentioned in the Realization of the Flutter Framework. The Widget in Flutter is a configuration description from which the Element class is generated. Similarly, Page is a description used to generate the actual routing object.
Understand this, and you’ll see that Navigator2.0 is simpler, not more complicated, to manage. As soon as we manipulate the Page list, the corresponding routing stack will sense it and automatically change. We simply place the page we want to display at the last element of the List. Navigator2.0 turns the black box of routing stacks into a List of lists. We want to change the order of pages in the routing stack by simply changing the position of the elements in List
Next we need to look at how to create objects using the Page class. The Page class itself descends from the RouteSettings class, indicating that it is indeed a routing configuration file. It is an abstract class and cannot be instantiated. We found two classes that directly implement it:
We can see that Flutter already provides a Material style for Android and a Cupertino style for iOS implementation classes.
Wrapper the Page we wrote directly with the MaterialPage, adding a method to encapsulate the logic to help create the Page:
MaterialPage _createPage(RouteSettings routeSettings) {
Widget child;
switch (routeSettings.name) {
case '/home':
child = const Home();
break;
case '/splash':
child = const Splash();
break;
case '/login':
child = const Login();
break;
case '/details':
child = Details(routeSettings.arguments! as Map<String.String>);
break;
default:
child = const Scaffold();
}
return MaterialPage(
child: child,
key: Key(routeSettings.name!) as LocalKey,
name: routeSettings.name,
arguments: routeSettings.arguments,
);
}
Copy the code
The processing here is somewhat similar to the static routing configuration table, but note that we can add the RouteSettings parameter to the page constructor, which is much more flexible than the 1.0 static routing.
Well, there are only a few ways to manipulate the Page list:
/// Press into a new page to display
void push({required String name, dynamic arguments}) {
_pages.add(_createPage(RouteSettings(name: name, arguments: arguments)));
// Notify the routing stack that our Page list has been changed
notifyListeners();
}
/// Replace the page that is currently being displayed
void replace({required String name, dynamic arguments}) {
if (_pages.isNotEmpty) {
_pages.removeLast();
}
push(name: name,arguments: arguments);
}
Copy the code
Finally, you need to make changes to main.dart in order to use the Navigator2.0 interface. Here, we instantiate a MyRouterDelegate global variable in app.dart for simplicity:
import 'package:nav2_demo/router/delegate.dart';
MyRouterDelegate delegate = MyRouterDelegate();
Copy the code
Alter main.dart to reference the global variable directly:
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key) {
// Add the first page at initialization
delegate.push(name: '/splash');
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'the Navigator 2.0',
debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: Router( routerDelegate: delegate, backButtonDispatcher: RootBackButtonDispatcher(), ), ); }}Copy the code
Create a Router declaratively and set the routerDelegate attribute. BackButtonDispatcher is not required here, but our example creates a default implementation of RootBackButtonDispatcher() to demonstrate the handling of the return key.
Okay, we’re almost done. Replace all the places where we used to operate the routing stack with Navigator1.0 with our own interface.
// Display the home page
delegate.replace(name: '/home');
Copy the code
Including where we passed arguments to the detail page constructor using dynamic routing earlier:
/// home.dart
GestureDetector(
onTap: (){
delegate.push(name: '/details',arguments: {'name':_movieList! [i].name,'imgUrl':_movieList! [i].imgUrl}); }, child: Column( mainAxisSize: MainAxisSize.min, children: [ Flexible(child: Image.network(_movieList![i].imgUrl)), Text(_movieList![i].name), ], ), );Copy the code
Details page receive data:
class Details extends StatelessWidget {
final String name;
final String imgSrc;
Details(Map<String.String> arguments)
: name = arguments['name']! , imgSrc = arguments['imgUrl']! ;/// Omit some code
}
Copy the code
Can you see that? We can now easily pass parameters to page constructs in a static routing manner similar to 1.0.
This is the basic use of Navigator2.0. Is it still hard to understand?
Here’s the complete code, and we’ve also rewritten the popRoute method to handle the logic of the page exit. Most of the time, we do not want the user to directly exit the App when clicking the back button. The following is done. When the user has returned to the root routing page, a dialog box will pop up asking the user whether he is sure to exit the App. The processing in 1.0 was very inelegant and needed to be wrapped by WillPopScope. Now it is no longer needed and can be handled directly in popRoute.
class MyRouterDelegate extends RouterDelegate<List<RouteSettings>> with ChangeNotifier.PopNavigatorRouterDelegateMixin<List<RouteSettings>> {
final List<Page> _pages = [];
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: List.of(_pages),
onPopPage: _onPopPage,
);
}
@override
Future<void> setNewRoutePath(List<RouteSettings> configuration) async {}
@override
Future<bool> popRoute() {
if (canPop()) {
_pages.removeLast();
notifyListeners();
return Future.value(true);
}
return _confirmExit();
}
bool canPop() {
return _pages.length > 1;
}
bool _onPopPage(Route route, dynamic result) {
if(! route.didPop(result))return false;
if (canPop()) {
_pages.removeLast();
return true;
} else {
return false; }}void push({required String name, dynamic arguments}) {
_pages.add(_createPage(RouteSettings(name: name, arguments: arguments)));
notifyListeners();
}
void replace({required String name, dynamic arguments}) {
if (_pages.isNotEmpty) {
_pages.removeLast();
}
push(name: name,arguments: arguments);
}
MaterialPage _createPage(RouteSettings routeSettings) {
Widget child;
switch (routeSettings.name) {
case '/home':
child = const Home();
break;
case '/splash':
child = const Splash();
break;
case '/login':
child = const Login();
break;
case '/details':
child = Details(routeSettings.arguments! as Map<String.String>);
break;
default:
child = const Scaffold();
}
return MaterialPage(
child: child,
key: Key(routeSettings.name!) as LocalKey,
name: routeSettings.name,
arguments: routeSettings.arguments,
);
}
Future<bool> _confirmExit() async {
final result = await showDialog<bool>( context: navigatorKey.currentContext! , builder: (context) {return AlertDialog(
content: const Text('Are you sure you want to quit the App? '),
actions: [
TextButton(
child: const Text('cancel'),
onPressed: () => Navigator.pop(context, true),
),
TextButton(
child: const Text('sure'),
onPressed: () => Navigator.pop(context, false),),,); });return result ?? true; }}Copy the code
Use in-depth
In the above case, we still haven’t solved the Web version problem. When we enter the URL in the browser address bar, we cannot locate the specific routing page. When we switch to a specific routing page, the URL of the address bar does not change synchronously. If your application is going to be web-compatible in the future, it’s important to continue learning more about Navigator2.0.
To handle this, we need to customize a routing information resolver:
/// parser.dart
class MyRouteInformationParser extends RouteInformationParser<List<RouteSettings>> {
const MyRouteInformationParser() : super(a);@override
Future<List<RouteSettings>> parseRouteInformation(RouteInformation routeInformation) {
final uri = Uri.parse(routeInformation.location!) ;if (uri.pathSegments.isEmpty) {
return Future.value([const RouteSettings(name: '/home')]);
}
final routeSettings = uri.pathSegments
.map((pathSegment) => RouteSettings(
name: '/$pathSegment',
arguments: pathSegment == uri.pathSegments.last
? uri.queryParameters
: null,
))
.toList();
return Future.value(routeSettings);
}
@override
RouteInformation restoreRouteInformation(List<RouteSettings> configuration) {
final location = configuration.last.name;
final arguments = _restoreArguments(configuration.last);
return RouteInformation(location: '$location$arguments');
}
String _restoreArguments(RouteSettings routeSettings) {
if(routeSettings.name ! ='/details') return ' ';
var args = routeSettings.arguments as Map;
return '? name=${args['name']}&imgUrl=${args['imgUrl']}'; }}Copy the code
There are two methods that need to be implemented, respectively
-
ParseRouteInformation: Helps us translate a URL address into a routing state (configuration information)
-
RestoreRouteInformation: Helps us convert the state (configuration information) of a route to a URL address
It can be seen that the functions of these two methods are just opposite, and exactly correspond to our two requirements: enter the URL to switch the corresponding routing page; Operation routing page, URL synchronization changes.
Specifically, the parseRouteInformation method receives a RouteInformation type parameter that describes information about a URL containing two properties, the string location and the dynamic type state. Location is the path part of the URL, and state is used to save the state in the page. For example, there is an input box in the page, and the input content in the input box is saved in state. When the page is restored next time, the data can also be restored. Knowing the parameters to this method, the code implementation above is easy to understand. It’s easier to parse the URL’s path to a Uri than to manipulate the string path directly, and then generate the corresponding routing configuration RouteSettings and return it.
The restoreRouteInformation method is simpler in that it takes a set of route configurations as parameters, and I need to combine them to generate a URL that I return as a RouteInformation object. The URL returned here is the URL used to update the browser’s address bar.
Now that we have our routing information parser in place, we need to add code to MyRouterDelegate:
@override
List<Page> get currentConfiguration => List.of(_pages);
@override
Future<void> setNewRoutePath(List<RouteSettings> configuration) {
debugPrint('setNewRoutePath ${configuration.last.name}');
_setPath(configuration
.map((routeSettings) => _createPage(routeSettings))
.toList());
return Future.value(null);
}
void _setPath(List<Page> pages) {
_pages.clear();
_pages.addAll(pages);
if(_pages.first.name ! ='/') {
// _pages.insert(0, _createPage(const RouteSettings(name: '/')));
}
notifyListeners();
}
Copy the code
First we need to override the currentConfiguration get method, which returns our Page list, and then implement the setNewRoutePath method that we left empty.
The call to parseRouteInformation, which was implemented earlier in the routing information parser, is followed by a callback to the setNewRoutePath method here, and obviously, The return value of the parseRouteInformation method is the parameter that is forwarded to the setNewRoutePath method. We’ve parsed the URL in the parseRouteInformation method and generated a set of routing configuration information, which is now forwarded to setNewRoutePath, which means that in setNewRoutePath, Generate the corresponding Page object of this group of routing configuration information, and insert it into the current Page list, and finally realize the routing stack update. The whole process can be summarized in one sentence, that is, a URL input from the outside, which eventually leads to the generation and update of the routing page in the App.
Finally, modify main.dart and set our routing information resolver:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key) {
delegate.push(name: '/splash');
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'the Navigator 2.0',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerDelegate: delegate,
routeInformationParser: constMyRouteInformationParser(), ); }}Copy the code
Here, we directly replace the router, the new constructor provided by the MaterialApp. Use this constructor to omit the RootBackButtonDispatcher we set earlier.
Switch App route, the address bar is also updated synchronously:
Enter the URL to navigate to the corresponding route within the App:
Perfect! Full source code access nav2_demo
Use the summary
- implementation
RouterDelegate
: it is a proxy for the route, and we must implement this class to manage the route using the Navigator 2.0 interface. In fact, it’s also an observed, it’s managed internallyPage
List, only modifiedPage
Lists do not cause real updates to the underlying routing stack, so we need to blend inChangeNotifier
, the operation is finishedPage
After the list, also callnotifyListeners()
Notifies the observer of the data change, triggering a real update of the underlying routing stack. And in our case code, it’s mixed inPopNavigatorRouterDelegateMixin
“, mainly to rewrite itpopRoute
Method, custom unstack logic. - implementation
RouteInformationParser
: it is a routing information parser. Its main purpose is to handle urls.
Note that Navigator 2.0’s interface works better with the state management framework, which is not incorporated in this article for simplicity.
Overall, Navigator 2.0 is not complex. It makes Flutter routing management more convenient and flexible. Based on this mechanism, third-party developers can build a highly encapsulated framework for remote and dynamic routing navigation.
Follow wechat official account: “The path of programming from 0 to 1”
Don’t be disappointed with the dry goods tutorial!
Or follow video lessons from bloggers