This article was first published on the official wechat account “MeandNi”. Click here to read it.

With the release of the latest version of the Flutter, various new features have come into view. This is the biggest change to the routing API. According to the official design document, the traditional command approach does not give developers a flexible way to directly manage the routing stack and is considered outdated. No Flutter at all.

As mentioned by a participant in one of Flutter’s user studies, the API also feels outdated and not very Flutter-y.

— Flutter Navigator 2.0 and Router

Navigator 2.0 introduces a new set of declarative apis that, unlike before, can translate the Navigator’s Routes into actual code by representing historical routing pages in an application with a declarative, immutable list of pages. This is in line with the principle that Flutter interprets immutable Widgets into Elements and renders them on the page. In this article, we’ll start with Navigator 1.0 and learn how to implement Navigator 2.0.

For the principles section see the latest article, the complete guide and principles of Flutter Navigator2.0

The Navigator 1.0

Before The launch of Navigator 2.0, we typically used the following two classes to manage individual pages in our applications:

  • Navigator, manage a groupRouteObject.
  • Route, aRouteUsually can represent a page, byNavigatorHold, the two commonly used implementation classes areMaterialPageRouteCupertinoPageRoute

Routes can be pushed into or popped out of the Navigator routing stack by naming routes and component routes (anonymous routes). Let’s take a quick look at the traditional usage.

Component routing

In the traditional routing implementation, the pages of the Flutter are organized by the Navigator and stacked on top of each other to form a “routing stack”. The common root component of the MaterialApp and CupertinoApp is the Navigator to manage the global routing. Navigation.of (), navigation.push (), navigation.pop () and other interfaces can be used to achieve navigation between multiple pages, as follows:

import 'package:flutter/material.dart';

void main() {
  runApp(Nav2App());
}

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnMaterialApp( home: HomeScreen(), ); }}class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('View Details'),
          onPressed: () {
            // Open the page
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) {
                returnDetailScreen(); })); },),),); }}class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('Pop! '),
          onPressed: () {
            // A page is displayedNavigator.pop(context); },),),); }}Copy the code

When the push() method is called, the DetailScreen component is placed on top of the HomeScreen, as shown in the figure.

At this point, the previous page (HomeScreen) is still in the component tree, so its state will remain when DetailScreen is opened.

After routing

The Flutter also allows pages to be opened by naming routes. The names of each page form a “routing table” that is passed to the MaterialApp and CupertinoApp by parameters (Routes) as follows:

import 'package:flutter/material.dart';

void main() {
  runApp(Nav2App());
}

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      / / the routing table
      routes: {
        '/': (context) => HomeScreen(),
        '/details': (context) => DetailScreen(), }, ); }}class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('View Details'),
          onPressed: () {
            Navigator.pushNamed(
              context,
              '/details',); },),),); }}class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('Pop! '), onPressed: () { Navigator.pop(context); },),),); }}Copy the code

Using named routes requires a pre-defined page to open. Although we can pass data between pages, this does not allow direct resolution of route parameters. For example, we cannot use the link /details/: ID route name in Web applications.

onGenerateRoute

Of course, we can use onGenerateRoute to accept the full path of the route, as follows:

onGenerateRoute: (settings) {
  // Handle '/'
  if (settings.name == '/') {
    return MaterialPageRoute(builder: (context) => HomeScreen());
  }
  
  // Handle '/details/:id'
  var uri = Uri.parse(settings.name);
  if (uri.pathSegments.length == 2 &&
      uri.pathSegments.first == 'details') {
    var id = uri.pathSegments[1];
    return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
  }
  
  return MaterialPageRoute(builder: (context) => UnknownScreen());
},
Copy the code

The complete code is as follows:

import 'package:flutter/material.dart';

void main() {
  runApp(Nav2App());
}

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: (settings) {
        // Handle '/'
        if (settings.name == '/') {
          return MaterialPageRoute(builder: (context) => HomeScreen());
        }

        // Handle '/details/:id'
        var uri = Uri.parse(settings.name);
        if (uri.pathSegments.length == 2 &&
            uri.pathSegments.first == 'details') {
          var id = uri.pathSegments[1];
          return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
        }

        returnMaterialPageRoute(builder: (context) => UnknownScreen()); }); }}class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('View Details'),
          onPressed: () {
            Navigator.pushNamed(
              context,
              '/details/1',); },),),); }}class DetailScreen extends StatelessWidget {
  String id;

  DetailScreen({
    this.id,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Viewing details for item $id'),
            FlatButton(
              child: Text('Pop! '), onPressed: () { Navigator.pop(context); },),],),); }}class UnknownScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text('404! '),),); }}Copy the code

Here, we can use the RouteSettings object Settings to get the parameters passed in when we call Navigator. PushNamed.

The Navigator 2.0

Navigator 2.0 provides a new set of interfaces for making route state part of application state and for parameter resolution through the underlying apis. The new apis are:

  • Page, which represents the configuration information for individual pages in the Navigator routing stack.

  • Router, which is used to formulate a list of pages to be displayed by the Navigator, usually changing as the state of the system or application changes.

  • The RouteInformationParser, which holds the RouteInformation provided by the RouteInformationProvider, can be parsed into the data types we define.

  • RouterDelegate, which defines the routing behavior in the application, such as how the Router knows about changes in the application state and how to respond. The main job is to listen to the RouteInformationParser and application state and build from the current page list.

  • The BackButtonDispatcher responds to the back button and notifies the Router

The following figure shows how the RouterDelegate interacts with the Router, RouteInformationParser, and the use state.

The general process is as follows:

  1. When the system opens a new page such as"Books / 2"),RouteInformationParserConverts it to the specific data type T in the application (e.gBooksRoutePath).
  2. The data type will be passed toRouterDelegatesetNewRoutePathMethod, where we can update the route state (e.g., by settingselectedBookId) and callnotifyListenersRespond to the operation.
  3. notifyListenersWill informRouterThe reconstructionRouterDelegate(bybuild()Methods).
  4. RouterDelegate.build()Return a newNavigatorExample, and finally show the page we want to open (e.gselectedBookId).

The Navigator 2.0 of actual combat

Next, let’s do a little exercise with Navigator 2.0. We’ll implement a Flutter application that, when applied to the Web, aligns the routing state with the URL connections in the browser and also handles the browser’s fallback button, as follows:

Next, switch the Flutter to the Master version using the Flutter Channel Master and create a Flutter project that supports Web applications. The code in Lib /main.dart is as follows:

import 'package:flutter/material.dart';

void main() {
  runApp(BooksApp());
}

class Book {
  final String title;
  final String author;

  Book(this.title, this.author);
}

class BooksApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _BooksAppState();
}

class _BooksAppState extends State<BooksApp> {
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Books App',
      home: Navigator(
        pages: [
          MaterialPage(
            key: ValueKey('BooksListPage'), child: Scaffold(), ) ], onPopPage: (route, result) => route.didPop(result), ), ); }}Copy the code

Pages

The Navigator takes a pages parameter. If the list of pages changes, the Navigator also needs to update the current routing stack to keep it in sync. Let’s use this property to develop an application that displays a single list of books in a new project

_BooksAppState holds two state parameters: a single list of books and the currently selected books:

class _BooksAppState extends State<BooksApp> {
  // New:
  Book _selectedBook;
  bool show404 = false;
  List<Book> books = [
    Book('Stranger in a Strange Land'.'Robert A. Heinlein'),
    Book('Foundation'.'Isaac Asimov'),
    Book('Fahrenheit 451'.'Ray Bradbury')];// ...
Copy the code

Then, in _BooksAppState, return a Navigator with a list of Page objects:

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Books App',
    home: Navigator(
      pages: [
        MaterialPage(
          key: ValueKey('BooksListPage'),
          child: BooksListScreen(
            books: books,
            onTapped: _handleBookTapped,
          ),
        ),
      ],
    ),
  );
}

void _handleBookTapped(Book book) {
    setState(() {
      _selectedBook = book;
    });
  }
// ...
class BooksListScreen extends StatelessWidget {
  final List<Book> books;
  final ValueChanged<Book> onTapped;
BooksListScreen({
    @required this.books,
    @required this.onTapped,
  });
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: ListView(
        children: [
          for (var book inbooks) ListTile( title: Text(book.title), subtitle: Text(book.author), onTap: () => onTapped(book), ) ], ), ); }}Copy the code

Since this application will have two pages (a single list of books and a details page), if a book is selected (using Collection If), the details page will be added:

pages: [
  MaterialPage(
    key: ValueKey('BooksListPage'),
    child: BooksListScreen(
      books: books,
      onTapped: _handleBookTapped,
    ),
  ),
	// New:
  if (show404)
    MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
  else if(_selectedBook ! =null)
    MaterialPage(
        key: ValueKey(_selectedBook),
        child: BookDetailsScreen(book: _selectedBook))
],
Copy the code

Note that the key here is defined by the value in the Book object as the unique identification of the MaterialPage, that is, the MaterialPage differs from the book object. Without a unique key, the frame cannot determine when to display transition animations between different pages.

We can also inherit the Page to implement custom behavior, for example, by adding a custom transition animation to the Page:

class BookDetailsPage extends Page {
  final Book book;
  
  BookDetailsPage({
    this.book,
  }) : super(key: ValueKey(book));
  
  Route createRoute(BuildContext context) {
    return PageRouteBuilder(
      settings: this,
      pageBuilder: (context, animation, animation2) {
        final tween = Tween(begin: Offset(0.0.1.0), end: Offset.zero);
        final curveTween = CurveTween(curve: Curves.easeInOut);
        returnSlideTransition( position: animation.drive(curveTween).drive(tween), child: BookDetailsScreen( key: ValueKey(book), book: book, ), ); }); }}Copy the code

Note that passing only the pages argument instead of onPopPage will also cause an error. It accepts a callback function that will be invoked every time navigator-pop () is called, where we can update the route status

Finally, it is an error to provide parameters when pages does not provide an onPopPage callback. This function is called by navigator.pop () each time it is called. It should be used to update the status (modify the page list), here we need to call the didPop method to determine if the pop was successful:

onPopPage: (route, result) {
  if(! route.didPop(result)) {return false;
  }

  // Update the list of pages by setting _selectedBook to null
  setState(() {
    _selectedBook = null;
  });

  return true;
},
Copy the code

We also have to check for a pop failure before updating the application state. Here, we use the setState method to notify the Flutter to call the build() method, where _selectedBook is null to show the book single-column table page.

The complete code is as follows:

import 'package:flutter/material.dart';

void main() {
  runApp(BooksApp());
}

class Book {
  final String title;
  final String author;

  Book(this.title, this.author);
}

class BooksApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _BooksAppState();
}

class _BooksAppState extends State<BooksApp> {
  Book _selectedBook;

  List<Book> books = [
    Book('Stranger in a Strange Land'.'Robert A. Heinlein'),
    Book('Foundation'.'Isaac Asimov'),
    Book('Fahrenheit 451'.'Ray Bradbury')];@override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Books App',
      home: Navigator(
        pages: [
          MaterialPage(
            key: ValueKey('BooksListPage'),
            child: BooksListScreen(
              books: books,
              onTapped: _handleBookTapped,
            ),
          ),
          if(_selectedBook ! =null) BookDetailsPage(book: _selectedBook)
        ],
        onPopPage: (route, result) {
          if(! route.didPop(result)) {return false;
          }

          // Update the list of pages by setting _selectedBook to null
          setState(() {
            _selectedBook = null;
          });

          return true; },),); }void_handleBookTapped(Book book) { setState(() { _selectedBook = book; }); }}class BookDetailsPage extends Page {
  final Book book;

  BookDetailsPage({
    this.book,
  }) : super(key: ValueKey(book));

  Route createRoute(BuildContext context) {
    return MaterialPageRoute(
      settings: this,
      builder: (BuildContext context) {
        returnBookDetailsScreen(book: book); }); }}class BooksListScreen extends StatelessWidget {
  final List<Book> books;
  final ValueChanged<Book> onTapped;

  BooksListScreen({
    @required this.books,
    @required this.onTapped,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: ListView(
        children: [
          for (var book inbooks) ListTile( title: Text(book.title), subtitle: Text(book.author), onTap: () => onTapped(book), ) ], ), ); }}class BookDetailsScreen extends StatelessWidget {
  final Book book;

  BookDetailsScreen({
    @required this.book,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if(book ! =null)... [ Text(book.title, style: Theme.of(context).textTheme.headline6), Text(book.author, style: Theme.of(context).textTheme.subtitle1), ], ], ), ), ); }}Copy the code

So far, we’ve implemented declarative routing management, but at this point we can’t handle the browser back button or synchronize links in the browser address block.

Router

In this section, we implement the RouteInformationParser, RouterDelegate update route state, achieve synchronization with the browser address bar link

The data type

First, we need to parse the routing information into the specified data type using RouteInformationParser:

class BookRoutePath {
  final int id;
  final bool isUnknown;

  BookRoutePath.home()
      : id = null,
        isUnknown = false;

  BookRoutePath.details(this.id) : isUnknown = false;

  BookRoutePath.unknown()
      : id = null,
        isUnknown = true;

  bool get isHomePage => id == null;

  bool getisDetailsPage => id ! =null;
}
Copy the code

In this application, we can use the BookRoutePath class to represent routing paths in the application, and we can implement parent and child classes to relate other types of routing information.

RouterDelegate

Next, we implement a BookRouterDelegate subclass of RouterDelegate:

class BookRouterDelegate extends RouterDelegate<BookRoutePath>
    with ChangeNotifier.PopNavigatorRouterDelegateMixin<BookRoutePath> {
  @override
  Widget build(BuildContext context) {
    // TODO
    throw UnimplementedError();
  }

  @override
  // TODO
  GlobalKey<NavigatorState> get navigatorKey => throw UnimplementedError();

  @override
  Future<void> setNewRoutePath(BookRoutePath configuration) {
    // TODO
    throwUnimplementedError(); }}Copy the code

The BookRouterDelegate’s generic type is BookRoutePath, which contains all the state needed to decide which page to display.

At this point, we can place the routing-related logic from _BooksAppState into a BookRouterDelegate, where we create a GlobalKey object and store all other states in it:

class BookRouterDelegate extends RouterDelegate<BookRoutePath>
    with ChangeNotifier.PopNavigatorRouterDelegateMixin<BookRoutePath> {
  final GlobalKey<NavigatorState> navigatorKey;

  Book _selectedBook;
  bool show404 = false;

  List<Book> books = [
    Book('Stranger in a Strange Land'.'Robert A. Heinlein'),
    Book('Foundation'.'Isaac Asimov'),
    Book('Fahrenheit 451'.'Ray Bradbury')]; BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();// ...
Copy the code

To display the correct path in the URL, we also need to return a BookRoutePath object based on the current state of the application:

BookRoutePath get currentConfiguration {
  if (show404) {
    return BookRoutePath.unknown();
  }

  return _selectedBook == null
      ? BookRoutePath.home()
      : BookRoutePath.details(books.indexOf(_selectedBook));
}
Copy the code

Below, the build method returns a Navigator component:

@override
Widget build(BuildContext context) {
  return Navigator(
    key: navigatorKey,
    pages: [
      MaterialPage(
        key: ValueKey('BooksListPage'),
        child: BooksListScreen(
          books: books,
          onTapped: _handleBookTapped,
        ),
      ),
      if (show404)
        MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
      else if(_selectedBook ! =null)
        BookDetailsPage(book: _selectedBook)
    ],
    onPopPage: (route, result) {
      if(! route.didPop(result)) {return false;
      }

      // Update the list of pages by setting _selectedBook to null
      _selectedBook = null;
      show404 = false;
      notifyListeners();

      return true; }); }Copy the code

Since this class is not a component, but is implemented by the ChangeNotifier, the onPopPage method here needs to use notifyListeners instead of setState to change the state. When the RouterDelegate triggers a state update, The Router also triggers the RouterDelegate’s currentConfiguration method and calls the Build method to create a new Navigator component.

The _handleBookTapped method also needs to use notifyListeners instead of setState:

void _handleBookTapped(Book book) {
  _selectedBook = book;
  notifyListeners();
}
Copy the code

When the new page opens, the Router calls the setNewRoutePath method to update the routing status of the application:

@override
Future<void> setNewRoutePath(BookRoutePath path) async {
  if (path.isUnknown) {
    _selectedBook = null;
    show404 = true;
    return;
  }

  if (path.isDetailsPage) {
    if (path.id < 0 || path.id > books.length - 1) {
      show404 = true;
      return;
    }

    _selectedBook = books[path.id];
  } else {
    _selectedBook = null;
  }

  show404 = false;
}
Copy the code

RouteInformationParser

The RouteInformationParser contains an internal hook function that receives routing information, which we can convert to a specific data type (BookRoutePath) and parse using a Uri:

class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
  @override
  Future<BookRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.location);
    // Handle '/'
    if (uri.pathSegments.length == 0) {
      return BookRoutePath.home();
    }

    // Handle '/book/:id'
    if (uri.pathSegments.length == 2) {
      if (uri.pathSegments[0] != 'book') return BookRoutePath.unknown();
      var remaining = uri.pathSegments[1];
      var id = int.tryParse(remaining);
      if (id == null) return BookRoutePath.unknown();
      return BookRoutePath.details(id);
    }

    // Handle unknown routes
    return BookRoutePath.unknown();
  }

  @override
  RouteInformation restoreRouteInformation(BookRoutePath path) {
    if (path.isUnknown) {
      return RouteInformation(location: '/ 404');
    }
    if (path.isHomePage) {
      return RouteInformation(location: '/');
    }
    if (path.isDetailsPage) {
      return RouteInformation(location: '/book/${path.id}');
    }
    return null; }}Copy the code

This implementation is only for this application and is not a regular route resolution solution. We will learn more about the specific principles later. Finally, to use these defined classes, we need to use the new MaterialApp.router constructor and pass in their respective implementations:

return MaterialApp.router(
  title: 'Books App',
  routerDelegate: _routerDelegate,
  routeInformationParser: _routeInformationParser,
);
Copy the code

The complete code is as follows:

import 'package:flutter/material.dart';

void main() {
  runApp(BooksApp());
}

class Book {
  final String title;
  final String author;

  Book(this.title, this.author);
}

class BooksApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _BooksAppState();
}

class _BooksAppState extends State<BooksApp> {
  BookRouterDelegate _routerDelegate = BookRouterDelegate();
  BookRouteInformationParser _routeInformationParser =
      BookRouteInformationParser();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Books App', routerDelegate: _routerDelegate, routeInformationParser: _routeInformationParser, ); }}class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
  @override
  Future<BookRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.location);
    // Handle '/'
    if (uri.pathSegments.length == 0) {
      return BookRoutePath.home();
    }

    // Handle '/book/:id'
    if (uri.pathSegments.length == 2) {
      if (uri.pathSegments[0] != 'book') return BookRoutePath.unknown();
      var remaining = uri.pathSegments[1];
      var id = int.tryParse(remaining);
      if (id == null) return BookRoutePath.unknown();
      return BookRoutePath.details(id);
    }

    // Handle unknown routes
    return BookRoutePath.unknown();
  }

  @override
  RouteInformation restoreRouteInformation(BookRoutePath path) {
    if (path.isUnknown) {
      return RouteInformation(location: '/ 404');
    }
    if (path.isHomePage) {
      return RouteInformation(location: '/');
    }
    if (path.isDetailsPage) {
      return RouteInformation(location: '/book/${path.id}');
    }
    return null; }}class BookRouterDelegate extends RouterDelegate<BookRoutePath>
    with ChangeNotifier.PopNavigatorRouterDelegateMixin<BookRoutePath> {
  final GlobalKey<NavigatorState> navigatorKey;

  Book _selectedBook;
  bool show404 = false;

  List<Book> books = [
    Book('Stranger in a Strange Land'.'Robert A. Heinlein'),
    Book('Foundation'.'Isaac Asimov'),
    Book('Fahrenheit 451'.'Ray Bradbury')]; BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>(); BookRoutePathget currentConfiguration {
    if (show404) {
      return BookRoutePath.unknown();
    }
    return _selectedBook == null
        ? BookRoutePath.home()
        : BookRoutePath.details(books.indexOf(_selectedBook));
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        MaterialPage(
          key: ValueKey('BooksListPage'),
          child: BooksListScreen(
            books: books,
            onTapped: _handleBookTapped,
          ),
        ),
        if (show404)
          MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
        else if(_selectedBook ! =null)
          BookDetailsPage(book: _selectedBook)
      ],
      onPopPage: (route, result) {
        if(! route.didPop(result)) {return false;
        }

        // Update the list of pages by setting _selectedBook to null
        _selectedBook = null;
        show404 = false;
        notifyListeners();

        return true; }); }@override
  Future<void> setNewRoutePath(BookRoutePath path) async {
    if (path.isUnknown) {
      _selectedBook = null;
      show404 = true;
      return;
    }

    if (path.isDetailsPage) {
      if (path.id < 0 || path.id > books.length - 1) {
        show404 = true;
        return;
      }

      _selectedBook = books[path.id];
    } else {
      _selectedBook = null;
    }

    show404 = false;
  }

  void_handleBookTapped(Book book) { _selectedBook = book; notifyListeners(); }}class BookDetailsPage extends Page {
  final Book book;

  BookDetailsPage({
    this.book,
  }) : super(key: ValueKey(book));

  Route createRoute(BuildContext context) {
    return MaterialPageRoute(
      settings: this,
      builder: (BuildContext context) {
        returnBookDetailsScreen(book: book); }); }}class BookRoutePath {
  final int id;
  final bool isUnknown;

  BookRoutePath.home()
      : id = null,
        isUnknown = false;

  BookRoutePath.details(this.id) : isUnknown = false;

  BookRoutePath.unknown()
      : id = null,
        isUnknown = true;

  bool get isHomePage => id == null;

  bool getisDetailsPage => id ! =null;
}

class BooksListScreen extends StatelessWidget {
  final List<Book> books;
  final ValueChanged<Book> onTapped;

  BooksListScreen({
    @required this.books,
    @required this.onTapped,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: ListView(
        children: [
          for (var book inbooks) ListTile( title: Text(book.title), subtitle: Text(book.author), onTap: () => onTapped(book), ) ], ), ); }}class BookDetailsScreen extends StatelessWidget {
  final Book book;

  BookDetailsScreen({
    @required this.book,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if(book ! =null)... [ Text(book.title, style: Theme.of(context).textTheme.headline6), Text(book.author, style: Theme.of(context).textTheme.subtitle1), ], ], ), ), ); }}class UnknownScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text('404! '),),); }}Copy the code

Now, running the example in Chrome will show you the currently visited link’s routing page and navigate to the correct page even after manually changing the URL.

TransitionDelegate

We can also use the TransitionDelegate to implement custom route animations.

First, pass a custom TransitionDelegate object to the Navigator:

// New:
TransitionDelegate transitionDelegate = NoAnimationTransitionDelegate();

      child: Navigator(
        key: navigatorKey,
        // New:
        transitionDelegate: transitionDelegate,
Copy the code

For example, the following implementation turns off all route transition animations:

class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
  @override
  可迭代<RouteTransitionRecord> resolve({
    List<RouteTransitionRecord> newPageRouteHistory,
    Map<RouteTransitionRecord, RouteTransitionRecord>
        locationToExitingPageRoute,
    Map<RouteTransitionRecord, List<RouteTransitionRecord>>
        pageRouteToPagelessRoutes,
  }) {
    final results = <RouteTransitionRecord>[];

    for (final pageRoute in newPageRouteHistory) {
      if (pageRoute.isWaitingForEnteringDecision) {
        pageRoute.markForAdd();
      }
      results.add(pageRoute);
    }

    for (final exitingPageRoute in locationToExitingPageRoute.values) {
      if (exitingPageRoute.isWaitingForExitingDecision) {
        exitingPageRoute.markForRemove();
        final pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
        if(pagelessRoutes ! =null) {
          for (final pagelessRoute in pagelessRoutes) {
            pagelessRoute.markForRemove();
          }
        }
      }

      results.add(exitingPageRoute);
    }
    returnresults; }}Copy the code

Here, NoAnimationTransitionDelegate rewrite the TransitionDelegate resolve () method, this method can specify whether the transition animation display various routing page, there are several ways:

  • MarkForPush, using transition animations when opening a page.

  • MarkForAdd, which opens the page without using transition animations.

  • MarkForPop, which uses an animation when a page is popped, notifies the application of passing the event to the AppRouterDelegate’s onPopPage function.

  • MarkForComplete, which does not use transition animations when the page pops up, also notifies the application.

  • MarkForRemove, no transition animations are used when the page pops up, and the application is not notified.

This class only affects the most recent declarative API, so the back button still shows the transition animation.

General process is as follows: here through all the pages of the statement, including a new page (newPageRouteHistory) and existing page (locationToExitingPageRoute), using the above several methods tag them. Using markForAdd removal in the new page open transition animations, is used in locationToExitingPageRoute markForRemove don’t show the transition animation, will not notify the application. See the full example.

read

Complete guide to Navigator2.0 with principle analysis

Learning Flutter’s new navigation and routing system

Watch! Release of Flutter 1.22

Three important trees in Flutter (rendering process, layout constraints, etc.)

Github.com/flutter/flu…

My WeChat: the original mp.weixin.qq.com/s/Fu9EFHJ4X…

Welcome to communicate and study with me.