【译】学习Flutter中新的Navigator和Router系统

原文:Learning Flutter’s new navigation and routing system
作者:John Ryan

本文解释了Flutter新的NavigatorRouter API是如何工作。如果你关注Flutter的开放设计文档,你可能已经看到这些新功能被称为Navigator 2.0和Router。我们将探讨这些API如何实现对应用程序中的屏幕进行更精细的控制,以及如何使用它来解析路由。

这些新的API并不是破坏性的更改,它只是增加了一个新的声明式API。在Navigator 2.0之前,很难push或pop多个页面,或者删除当前页面下面一个页面。如果你对Navigator目前的工作方式很满意,你可以继续以同样的方式(命令式的)使用它。

Router提供了处理来自底层平台的路由并显示相应页面的能力。在本文中,Router被配置为解析浏览器URL以显示相应的页面。

本文将帮助您选择哪种 Navigator 模式最适合您的应用程序,并解释了如何使用 Navigator 2.0 来解析浏览器 URL,并完全控制活动页面的堆栈。本文的例子展示了如何构建一个应用程序,以处理来自平台的传入路由并管理应用程序的页面。下面的GIF展示了这个示例程序的操作:

示例程序的操作

Navigator 1.0

如果你正在使用Flutter,那么您可能正在使用Navigator并熟悉以下概念:

  • Navigator - 管理路由对象堆栈的小部件。
  • Route - 由Navigator管理的对象,表示屏幕,通常由MaterialPagerRoute这样的类实现。

在Navigator 2.0之前,Route通过命名路由或匿名路由push和pop到Navigator的堆栈中。接下来的部分将简要回顾这两种方法。

匿名路由

大多数移动应用的页面都是彼此叠放在一起,就像堆栈一样。在Flutter中,使用Navigator很容易做到这一点。

MaterialAppCupertinoApp已经使用Navigator。您可以使用Navigator.of()访问它,也可以使用Navigator.push()显示一个新页面,并使用Navigator.pop()返回上一个页面。

mport 'package:flutter/material.dart';

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

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('View Details'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) {
                return DetailScreen();
              }),
            );
          },
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text('Pop!'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

当调用push()时,DetailScreen被放置在HomeScreen的顶部,就像这样。
在这里插入图片描述

上一个页面(HomeScreen)仍然是Widget树的一部分,所以当DetailScreen可见时,与它相关的任何State对象都会保持不变。

命名路由

Flutter也支持命名路由,它在MaterialAppCupertinoApp中的routes参数定义。

import 'package:flutter/material.dart';

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

class Nav2App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      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);
          },
        ),
      ),
    );
  }
}

这些路由必须是预先定义的。虽然您可以向命名路由传递参数,但不能从路由本身解析参数。例如,如果应用程序在Web上运行,你就不能从/details/:id这样的路由中解析ID。

使用onGenerateRoute的高级命名路由

处理命名路由更灵活的方法是使用onGenerateRoute。这个API让你有能力处理所有的路径,这下面是完整的示例:

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));
        }

        return MaterialPageRoute(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!'),
      ),
    );
  }
}

这里的settingsRouteSettings的一个实例。name和arguments字段是调用Navigator.pushNamed时提供的值,或者是initialRoute设置的值。

Navigator 2.0

Navigator 2.0 API为框架增加了新的类,以便使应用的屏幕成为应用状态的函数,并从底层提供解析路由的能力(如Web URL)。下面是对新内容的概述:

  • Page — 一个不可更改的对象,用于设置Navigator的历史堆栈。
  • Router — 配置要由Navigator显示的页面列表。通常此页面列表根据平台或应用的状态变化而变化。
  • RouteInformationParser,它从RouteInformationProvider中获取RouteInformation,并将其解析为用户定义的数据类型。
  • RouterDelegate — 定义了Router如何学习应用状态变化以及如何响应这些变化的应用特定行为。它的工作是监听RouteInformationParser和应用状态,并利用当前的Pages列表构建Navigator
  • BackButtonDispatcher — 向Router报告返回按钮按下的情况。

下图显示了RouterDelegate如何与RouterRouteInformationParser和应用的状态进行交互。

在这里插入图片描述

下面是这些部件如何交互的一个例子:

  1. 当平台发出一个新的路由(例如,“books/2”)时,RouteInformationParser将其转换为你在应用中定义的抽象数据类型T(例如名为BooksRoutePath的类)。
  2. RouterDelegatesetNewRoutePath方法是用这个数据类型调用的,必须更新应用程序状态以反映变化(例如,通过设置selectedBookId),并调用notifyListeners
  3. notifyListeners被调用时,它告诉Router重建RouterDelegate(使用其build()方法)。
  4. RouterDelegate.build()返回一个新的Navigator,其页面现在反映了应用状态的变化(例如selectedBookId)。

Navigator 2.0 练习

本节将带领你完成一个使用Navigator 2.0 API的练习。我们最终会得到一个可以与URL栏保持同步的应用,并处理来自应用和浏览器的返回按钮操作,如下图所示。

示例程序的操作

切换到master渠道创建一个支持web的Flutter项目,并将lib/main.dart的内容替换为以下内容:

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),
      ),
    );
  }
}

Pages

Navigator的构造函数中有一个pages参数。如果列表中的Page发生变化,Navigator就会更新堆栈的路由来匹配。为了了解其工作原理,我们将构建一个显示书籍列表的应用程序。

_BooksAppState中,保留两种状态:书籍列表和所选书籍。

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'),
  ];
  
  // ...

然后在_BooksAppState中,返回一个带有Page对象列表的Navigator

@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 in books)
            ListTile(
              title: Text(book.title),
              subtitle: Text(book.author),
              onTap: () => onTapped(book),
            )
        ],
      ),
    );
  }
}

由于这个应用有两个页面,一个是书的列表页面,一个是显示详情的页面。如果选择了一本书(使用collection if),就增加第二个(详情)页面。

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))
],

注意,页面的key是由Book对象的值定义的。这告诉Navigator,当Book对象不同时,这个MaterialPage对象和另一个对象是不同的。如果没有一个唯一的key,框架就无法决定何时在不同的Pages之间显示过渡动画。

注意:如果你喜欢,你也可以扩展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);
        return SlideTransition(
          position: animation.drive(curveTween).drive(tween),
          child: BookDetailsScreen(
            key: ValueKey(book),
            book: book,
          ),
        );
      },
    );
  }
}

最后,只提供pages参数而不提供onPopPage回调是一个错误。每当Navigator.pop()被调用时,这个函数就会被调用。它应该用来更新状态(决定页面列表),而且它必须调用路由上的didPop来确定pop是否成功。

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

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

  return true;
},

在更新应用程序状态之前,检查 didPop 是否失败是很重要的。

使用setState通知框架调用build()方法,当_selectedBook为空时,该方法返回一个带有单页的列表。

下面是完整的例子。

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) {
        return BookDetailsScreen(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 in books)
            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),
            ],
          ],
        ),
      ),
    );
  }
}

就目前而言,这个应用只能让我们以声明式定义页面的堆栈。我们无法处理平台的后退按钮,浏览器的URL也不会随着我们的Navigator而改变。

Router

到目前为止,该应用可以显示不同的页面,但不能处理来自底层平台的路由,比如用户更新浏览器中的URL。

本节介绍如何实现RouteInformationParserRouterDelegate以及更新应用状态。一旦设置好,应用程序就会与浏览器的URL保持同步。

数据类型

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 get isDetailsPage => id != null;
}

在这个应用中,应用中所有的路由都可以使用一个类来表示。相反,你也可以选择使用不同的类来实现一个超类,或者用另一种方式来管理路由信息。

RouterDelegate

接下来,添加一个扩展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
    throw UnimplementedError();
  }
}

RouterDelegate上定义的通用类型是BookRoutePath,其中包含决定显示哪些页面所需的所有状态。

我们需要将一些逻辑从_BooksAppState转移到BookRouterDelegate上,并创建一个GlobalKey。在这个例子中,应用状态直接存储在RouterDelegate上,但也可以分离到另一个类中。

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>();
  // ...

为了在URL中显示正确的路径,我们需要根据应用的当前状态返回一个BookRoutePath:

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

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

接下来,RouterDelegate中的build方法需要返回一个Navigator:

@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;
    },
  );
}

onPopPage回调现在使用notifyListeners而不是setState,因为这个类现在是一个ChangeNotifier,而不是一个widget。当 RouterDelegate 通知它的监听器时,同样会通知Router widget 。 RouterDelegatecurrentConfiguration 已经改变,需要再次调用其build 方法来构建新的 Navigator

_handleBookTapped方法也需要使用notifyListeners而不是setState

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

当一个新的路由被push时,Router会调用setNewRoutePath,这样我们的应用就有机会根据路由的变化来更新应用状态。

  @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;
  }

RouteInformationParser

RouteInformationParser提供了一个钩子来解析传入的路由(RouteInformation),并将其转换为用户定义的类型(BookRoutePath)。使用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;
  }
}

这个实现是针对这个应用的,而不是一般的路由解析解决方案。后面会有更多的介绍。

为了使用这些新的类,我们使用新的MaterialApp.router构造函数,并传入我们的自定义实现。

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

这是完整的示例:

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>();

  BookRoutePath get 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) {
        return BookDetailsScreen(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 get isDetailsPage => 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 in books)
            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!'),
      ),
    );
  }
}

在Chrome浏览器中运行这个示例,现在可以显示正在导航的路由,并在手动编辑URL时导航到正确的页面。

TransitionDelegate

您可以提供TransitionDelegate的自定义实现,自定义当页面列表发生变化时,路由如何出现在(或从)屏幕上。如果您需要自定义,请继续阅读,但如果您对默认行为满意,可以跳过本节。

为导航器提供一个自定义的TransitionDelegate,定义所需的行为:

// New:
TransitionDelegate transitionDelegate = NoAnimationTransitionDelegate();

      child: Navigator(
        key: navigatorKey,
        // New:
        transitionDelegate: transitionDelegate,

例如,下面的实现会禁用所有的过渡动画。

class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
  @override
  Iterable<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);
    }
    return results;
  }
}

这个自定义的实现覆盖了resolve(),它负责将各种路由标记为推送、弹出、添加、完成或删除。

  • markForPush - 显示路由时有动画过渡。
  • markForAdd - 显示路由时没有动画过渡。
  • markForPop - 删除路由时有动画过渡,并用result完成它。在此上下文中,"完成 "意味着result对象被传递给AppRouterDelegateonPopPage回调。
  • markForComplete - 删除路由时没有动画过渡,并用result来完成它。
  • markForRemove - 删除路由时没有动画过渡,且没有完成。

这个类只影响声明式API,这就是为什么点击后退按钮仍然显示过渡动画。

本示例的工作方式:这个例子既关注新的路由,也关注退出屏幕的路由。它遍历newPageRouteHistory中的所有对象,并使用markForAdd将它们标记为不需要过渡动画的添加。接下来,它遍历locationToExitingPageRoute map 的值。如果它找到一个标记为isWaitingForExitingDecision的路由,那么它就调用markForRemove来表示该路由应该在没有过渡和没有完成的情况下被删除。

下面是完整的示例(Gist)

嵌套的路由器

这个较大的演示程序展示了如何在一个Router中添加另一个Router。许多应用程序需要为BottomAppBar中的目的地提供路由,并为其上方的一堆视图提供路由,这需要两个导航器。为此,应用使用状态对象来存储应用特定的导航状态(选定的菜单索引和选定的Book对象)。这个例子还展示了如何配置哪个Router处理后退按钮。

嵌套路由示例(Gist)

接下来是什么

本文探讨了如何在特定的应用中使用这些API,但也可以用来构建更高级别的API包。我们希望你能和我们一起探讨在这些功能之上构建的更高级的API能为用户做什么。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值