学一学Flutter新的导航和路由系统

2022-09-20 16:44:20 浏览数 (1)

阅读大概需要9分钟

本文介绍了flutter中NavigatorRouterAPI是如何工作的。如果你一直在关注 Flutter 开放的设计文档[1],你可能已经看到了这些称为Navigator 2.0 和 Router 的[2]新功能。下面我们将探索这些 API 如何对应用中的视觉进行更精细的控制,以及如何使用它来解析路由。

这些新的 API 并没有破坏性的变化,只是添加了一个新的_声明性_API[3]。在 Navigator 2.0 之前,很难推送或弹出多个页面[4],或者删除当前页面下方的页面。但是,如果对Navigator的工作方式感到满意,也可以继续方式使用它。

Router提供了从底层平台处理方和显示相应页面的方法。在本文中,我们使用Router去解析浏览器 URL 并且显示相应的页面。

学完本文后,你将找到在你的APP中使用Navigator最好方式,并且可以掌握如何使用 Navigator 2.0 来解析浏览器 URL 并能完全控制激活中的页面栈。本文将通过一个示例来演示如何处理平台传入的路由并管理APP的页面。

Navigator 1.0

在 Flutter中,你一定知道Navigator的以下概念:

  • [**Navigator**](https://master-api.flutter.dev/flutter/widgets/Navigator-class.html "**Navigator**") — 管理一组 Route 对象的小部件。
  • [**Route**](https://master-api.flutter.dev/flutter/widgets/Route-class.html "**Route**") — 被Navigator管理的当前屏幕,Route的实现比如MaterialPageRoute.

在 Navigator 2.0 之前,页面使用【命名路由】或【匿名路由】进栈和出栈。接下来的部分是对这两种方法做一个简要的回顾。

匿名路由

在flutter中通过Navigator可以很轻松的实现路由管理.

MaterialAppCupertinoApp使用Navigator非常容易。可以使用[Navigator.of()](https://api.flutter.dev/flutter/widgets/Navigator/of.html "Navigator.of( "Navigator.of()")")或使用[Navigator.push()](https://api.flutter.dev/flutter/widgets/Navigator/push.html "Navigator.push( "Navigator.push()")")显示一个新页面,或者使用[Navigator.pop()](https://api.flutter.dev/flutter/services/SystemNavigator/pop.html "Navigator.pop( "Navigator.pop()")")返回上一个页面。

代码语言:javascript复制
import '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树的一部分,因此与State相关的都不会被销毁。

命名路由

Flutter 还支持命名路由,在MaterialAppCupertinoApproutes参数中进行定义 :

代码语言:javascript复制
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);
          },
        ),
      ),
    );
  }
}

这些路由必须预先定义。尽管可以将参数传递给命名的路由[5],但无法解析路由本身的参数。如/details/:id

使用 onGenerateRoute 的高级命名路由

处理命名路由的一种更灵活的方法是使用onGenerateRoute. 此 API 使您能够处理所有路径:

代码语言:javascript复制
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());
},

这是完整的示例:

代码语言:javascript复制
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!'),
      ),
    );
  }
}

这里,settings[RouteSettings](https://api.flutter.dev/flutter/widgets/RouteSettings-class.html "RouteSettings")的一个实例。name和arguments字段是在[Navigator.pushNamed](https://api.flutter.dev/flutter/widgets/Navigator/pushNamed.html "Navigator.pushNamed")调用时或[initialRoute](https://api.flutter.dev/flutter/material/MaterialApp/initialRoute.html "initialRoute")设置时提供。

Navigator 2.0

Navigator 2.0 API 在框架中添加了新类,以使APP的页面成为APP state的一个函数,并提供解析来自底层平台的路由(如 Web URL)的能力。以下是新功能的概述:

  • [**Page**](https://master-api.flutter.dev/flutter/widgets/Page-class.html "**Page**") — 用于设置导航历史堆栈的不可变对象。
  • [**Router**](https://master-api.flutter.dev/flutter/widgets/Router-class.html "**Router**")— 用于配置被导航展示的页面列表。通常这个页面列表会根据底层平台或应用程序的状态变化而变化。
  • [**RouteInformationParser**](https://master-api.flutter.dev/flutter/widgets/Router/routeInformationParser.html "**RouteInformationParser**"),它从[RouteInformationProvider](https://master-api.flutter.dev/flutter/widgets/RouteInformationProvider-class.html "RouteInformationProvider")接受[RouteInformation](https://master-api.flutter.dev/flutter/widgets/RouteInformation-class.html "RouteInformation")并将其解析为用户定义的数据类型。
  • [**RouterDelegate**](https://master-api.flutter.dev/flutter/widgets/RouterDelegate-class.html "**RouterDelegate**")— 定义特定Router的行为,即如何了解APP状态的变化以及它如何响应这些变化。它的工作是监听RouteInformationParser和 APP状态并让Navigator使用当前列表构建Pages
  • [**BackButtonDispatcher**](https://master-api.flutter.dev/flutter/widgets/BackButtonDispatcher-class.html "**BackButtonDispatcher**")— 向Router报告返回按钮的按下情况。

下图显示了如何RouterDelegate与交互RouterRouteInformationParser以及APP的状态:

以下是这些部分如何相互作用的示例:

  1. 当平台发出新路由(例如,“books/2”)时,它RouteInformationParser会将其转换为T即在APP中定义的数据类型(例如,名为BooksRoutePath的类)。
  2. RouterDelegatesetNewRoutePath方法使用此数据类型调用,并且必须更新APP状态以更改(例如,通过设置selectedBookId)并调用notifyListeners.
  3. notifyListeners被调用时,它告诉Router重建RouterDelegate(使用它的build()方法)
  4. RouterDelegate.build()返回一个新的 Navigator,其页面根据APP状态作出对应的更改(例如,selectedBookId)。

导航器 2.0 练习

本节将通过一个例子完成使用 Navigator 2.0 API 的练习。最终会完成一个可以与 URL 栏保持同步的app,并处理来自应用程序和浏览器的后退按钮按下,如下面的 GIF 所示:

接下来,创建一个带有 web 支持的新 Flutter 项目并将其中的内容替换lib/main.dart为以下内容:

代码语言:javascript复制
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会更新路由堆栈。我们通过构建一个显示书籍列表的app来展示它的工作原理。

在 中_BooksAppState,保持两个状态:书籍列表和选中的书籍:

代码语言:javascript复制
class _BooksAppState extends State<BooksApp> {
  // New:
  Book _selectedBook;
  bool show404 = false;
  List<Book> books = [
    Book('Left Hand of Darkness', 'Ursula K. Le Guin'),
    Book('Too Like the Lightning', 'Ada Palmer'),
    Book('Kindred', 'Octavia E. Butler'),
  ];
  
  // ...

然后在 中_BooksAppState,返回Navigator带有Page列表的 :

代码语言:javascript复制
@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),请显示第二个(详细信息)页面:

代码语言:javascript复制
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对象定义的。这告诉NavigatorBook对象不同时 MaterialPage 对象与另一个对象是不同的。如果没有唯一的Key,app就无法确定何时在不同的页面之间显示过渡动画。

注意:还可以为Page自定义行为。例如,为页面添加了一个自定义的过渡动画:

代码语言:javascript复制
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路由来确定弹出是否成功:

代码语言:javascript复制
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为 null时返回一个单页列表。

这是完整的示例:

代码语言:javascript复制
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('Left Hand of Darkness', 'Ursula K. Le Guin'),
    Book('Too Like the Lightning', 'Ada Palmer'),
    Book('Kindred', 'Octavia E. Butler'),
  ];

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

就目前而言,此app仅使我们能够以声明方式定义页面堆栈。我们无法处理平台的后退按钮,浏览器的 URL 在我们导航时也不会改变。

Router

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

本节将展示如何实现RouteInformationParserRouterDelegate并更新app的状态。设置后,app会与浏览器的 URL 保持同步。

数据类型

RouteInformationParser解析路由信息到用户定义的数据类型,所以我们先定义一个类:

代码语言:javascript复制
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;
}

在本app中,所有路由都可以使用一个类来表示。同样也可以选择基础的方式,或以其他方式管理路由信息。

RouterDelegate

接下来,添加一个继承自RouterDelegate的类:

代码语言:javascript复制
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. 在此示例中,APP状态直接存储在RouterDelegate上,也可以分离到另一个类中。

代码语言:javascript复制
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
  final GlobalKey<NavigatorState> navigatorKey;

  Book _selectedBook;
  bool show404 = false;

  List<Book> books = [
    Book('Left Hand of Darkness', 'Ursula K. Le Guin'),
    Book('Too Like the Lightning', 'Ada Palmer'),
    Book('Kindred', 'Octavia E. Butler'),
  ];

  BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
  // ...

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

代码语言:javascript复制
BookRoutePath get currentConfiguration {
    if (show404) {
      return BookRoutePath.unknown();
    }

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

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

代码语言:javascript复制
@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同样会通知RouterDelegate'scurrentConfiguration已更改并且build方法需要再次被调用构建新的Navigator.

_handleBookTapped方法也需要使用notifyListeners代替setState

代码语言:javascript复制
 void _handleBookTapped(Book book) {
    _selectedBook = book;
    notifyListeners();
  }

当一个新路由被推送到应用程序时,Router调用setNewRoutePath,这使我们的应用程序根据路由的更改更新应用程序状态:

代码语言:javascript复制
@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该类来处理解析:

代码语言:javascript复制
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构造函数并传入我们的自定义实现:

代码语言:javascript复制
 return MaterialApp.router(
      title: 'Books App',
      routerDelegate: _routerDelegate,
      routeInformationParser: _routeInformationParser,
    );

下面是完整的示例:

代码语言:javascript复制
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('Left Hand of Darkness', 'Ursula K. Le Guin'),
    Book('Too Like the Lightning', 'Ada Palmer'),
    Book('Kindred', 'Octavia E. Butler'),
  ];

  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中试试效果了。

TransitionDelegate

通过TransitionDelegate可以自定义页面过度动画。如果您需要对此进行自定义,请继续阅读,但如果您对默认行为感到满意,则可以跳过此部分。

使用方法如下:

代码语言:javascript复制
// New:
TransitionDelegate transitionDelegate = NoAnimationTransitionDelegate();

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

例如,以下实现禁用所有过渡动画:

代码语言:javascript复制
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对象被传递到 上的onPopPage回调AppRouterDelegate
  • markForComplete — 删除没有过渡的路线并用一个完成它 result
  • markForRemove — 删除没有动画过渡且未完成的路线。

这个类仅影响_声明式_API,这就是后退按钮仍显示过渡动画的原因。

参考资料

[1]

设计文档: https://flutter.dev/docs/resources/design-docs

[2]

Navigator 2.0 和 Router 的: https://docs.google.com/document/d/1Q0jx0l4-xymph9O6zLaOY4d_f7YFpNWX_eGbzYxr9wY/edit#heading=h.l6kdsrb6j9id

[3]

_声明性_API: https://flutter.dev/docs/get-started/flutter-for/declarative

[4]

很难推送或弹出多个页面: https://github.com/flutter/flutter/issues/12146

[5]

将参数传递给命名的路由: https://flutter.dev/docs/cookbook/navigation/navigate-with-arguments

-------------都到这了,点一下关注再走---------

0 人点赞