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

Posted 唯鹿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了译学习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: [
          Materia

以上是关于译学习Flutter中新的Navigator和Router系统的主要内容,如果未能解决你的问题,请参考以下文章

Flutter学习指南:交互手势和动画,安卓工程师的面试题

Flutter 一起使用 navigator 和 ChangeNotifierProvider 的最佳方法是啥

你不必担心Dart的垃圾回收器(译)

Flutter Navigator 2.0原理详解

Navigator.push 可以和 Flutter 三元一起使用吗

Flutter / Dart 使用具有后代和 Navigator 的 Scoped Model