Комплексный анализ Flutter Navigator 2.0

Flutter
Комплексный анализ Flutter Navigator 2.0

Эта статья была впервые опубликована в публичном аккаунте WeChat."Меанд Ни",нажмите, чтобы прочитать.

С выходом последней версии,Flutter 1.22Различные новые функции в .Не дает разработчикам гибкого способа прямого управления стеком маршрутизации., даже кажется устаревшим, совсем не Flutter.

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

А в Navigator 2.0 представлен новый набор декларативных API, которые, в отличие от предыдущих, могут реализовыватьПредставляет исторические страницы маршрутизации в приложении в виде декларативного и неизменного списка страниц., который преобразуется в Маршруты Навигатора в реальном коде, которыйОн совпадает с принципом разбора неизменяемых виджетов в элементы и рендеринга их на странице во Flutter., но Flutter заполнен. В этой статье мы начнем с Navigator 1.0 и постепенно разберемся с реализацией Navigator 2.0.

Содержание этой статьи переведено с:Изучение новой системы навигации и маршрутизации Flutter

Для основной части, пожалуйста, обратитесь к последней статье:Flutter Navigator2.0 Полное руководство и принципиальный анализ

Navigator 1.0

До появления Navigator 2.0 мы обычно использовали следующие два класса в нашем приложении для управления каждой страницей:

  • Navigator, которая управляет группойRouteобъект.
  • Route,ОдинRouteобычно может представлять страницу, котораяNavigatorДержите, два обычно используемых класса реализации:MaterialPageRouteа такжеCupertinoPageRoute.

RouteМожет быть добавлен или извлечен через именованные маршруты и маршруты компонентов (анонимные маршруты).Navigatorстек маршрутизации. Давайте кратко рассмотрим предыдущее традиционное использование.

маршрутизация компонентов

При реализации традиционной маршрутизации каждая страница Flutter состоит изNavigatorОрганизация, наложенная друг на друга в «стек маршрутизации», обычно используемые корневые компоненты.MaterialAppа такжеCupertinoAppдно черезNavigatorЧтобы добиться управления глобальной маршрутизацией, мы можем передатьNavigator.of()илиNavigator.push(),Navigator.pop()и другие интерфейсы для обеспечения навигации между несколькими страницами, конкретные методы заключаются в следующем:

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) все еще находится в дереве компонентов, поэтому, когдаDetailScreenЕго состояние по-прежнему сохраняется при открытии.

именованный маршрут

FLUTTER также поддерживает открытие страницы по именованной маршрутизации, а имя каждой страницы состоит из «таблицы маршрутизации» и передает параметр (routes).MaterialApp ,CupertinoApp,следующим образом:

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

При использовании именованных маршрутов необходимо предварительно определить страницы, которые необходимо открыть.Хотя мы также можем передавать данные между страницами, этот метод нативно не поддерживает прямой разбор параметров маршрутизации.Например, нельзя использовать форму ссылки в веб-приложениях. ./details/:idназвание маршрута.

onGenerateRoute

Конечно, мы можем использоватьonGenerateRouteпринять полный путь маршрута, как показано ниже:

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

Полный код выглядит следующим образом:

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

Здесь мы можем пройтиRouteSettingsтип объектаsettingsможет получитьNavigator.pushNamedПараметры, передаваемые при вызове.

Navigator 2.0

Navigator 2.0 предоставляет ряд новых интерфейсов, которые могут реализовывать состояние маршрутизации как часть состояния приложения и могут реализовывать функцию разбора параметров через базовый API.Новые API следующие:

  • Page, используется для представленияNavigatorИнформация о конфигурации каждой страницы в стеке маршрутизации.

  • Router, используемый для формулировкиNavigatorСписок отображаемых страниц, как правило, список страниц меняется в зависимости от состояния системы или приложения.

  • RouteInformationParser,держатьRouteInformationProviderкоторый предоставилRouteInformation, который может быть проанализирован до определенного нами типа данных.

  • RouterDelegate, который определяет поведение маршрутизации в приложении, например, как Router узнает об изменениях в состоянии приложения и как реагировать. Основная задача - следитьRouteInformationParserсостояние приложения и сборка из текущего списка страниц.

  • BackButtonDispatcher, ответить на кнопку "Назад" и уведомитьRouter

На рисунке ниже показаноRouterDelegateа такжеRouter,RouteInformationParserи принцип взаимодействия с государством,

Общий процесс выглядит следующим образом:

  1. Когда система открывает новую страницу (например,“books / 2”)Время,RouteInformationParserпреобразует его в конкретный тип данных T в приложении (например,BooksRoutePath).
  2. Тип данных будет переданRouterDelegateизsetNewRoutePathметод, где мы можем обновить состояние маршрутизации (например, установивselectedBookId) и звонитеnotifyListenersОтветьте на действие.
  3. notifyListenersуведомитRouterреконструкцияRouterDelegate(пройти черезbuild()метод).
  4. RouterDelegate.build()вернуть новыйNavigatorэкземпляр и, наконец, отобразить страницу, которую мы хотим открыть (например,selectedBookId).

Навигатор 2.0 в действии

Далее давайте используем Navigator 2.0 для небольшого упражнения. Мы реализуем приложение Flutter. Когда приложение действует в Интернете, состояние маршрутизации будет соответствовать URL-соединению в браузере, и оно также может обрабатывать резервный вариант браузера. кнопку следующим образом:

Далее используйтеflutter channel masterПереключите Flutter на основную версию,Создайте проект 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параметр, если список страниц изменяется,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))
],

Обратите внимание, что ключ здесь будет определяться значением в объекте книги какMaterialPageУникальный идентификатор объекта , т. е. объекта книги, отличается от приведенного здесь.MaterialPageПросто другой. Без уникального ключа платформа не может определить, когда анимировать переходы между разными страницами.

Мы также можем наследовать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()Эта функция запускается при вызове, где мы можем обновить состояние маршрутизации.

Наконец, вpagesНе предоставленonPopPageВ случае обратного вызова предоставление аргументов является ошибкой. каждый раз, когда его зовутNavigator.pop()вызвать эту функцию. Он должен использоваться для обновления состояния (изменения списка страниц), здесь нам нужно вызватьdidPopметод, чтобы определить, был ли поп успешным:

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

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

  return true;
},

Мы также должны проверить, не происходит ли всплывание, прежде чем обновлять состояние приложения. Здесь мы использовалиsetStateспособ уведомить Flutter о вызове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),
            ],
          ],
        ),
      ),
    );
  }
}

В настоящее время мы внедрили декларативное управление маршрутизацией, в настоящее время мы не можем обрабатывать кнопку «Назад» в браузере, а также не можем синхронизировать ссылки в адресном блоке браузера.

Router

В этом разделе мы реализуемRouteInformationParser, RouterDelegateОбновите статус маршрутизации для синхронизации со ссылками в адресной строке браузера.

тип данных

Сначала нам нужно пройти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;
}

В этом приложении вы можете использоватьBookRoutePathКласс для представления пути маршрутизации в приложении, мы также можем реализовать родительский и дочерний классы для связи других типов маршрутной информации.

RouterDelegate

Далее мы реализуемRouterDelegateподклассBookRouterDelegate:

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

BookRouterDelegateОбщий типBookRoutePath, который содержит все состояния, необходимые для выбора отображаемой страницы.

В этот момент мы можем_BooksAppStateЛогика маршрутизации вBookRouterDelegate, здесь мы создаемGlobalKeyобъект, а также в нем хранятся другие состояния:

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

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

Поскольку класс не является компонентом, онChangeNotifierреализовано, так что здесьonPopPageнеобходимо использовать методnotifyListenersзаменятьsetStateизменить состояние, когдаRouterDelegateКогда запускается обновление состояния,Routerтакже вызоветRouterDelegateизcurrentConfigurationметод и вызовbuildметод создания новогоNavigatorкомпоненты.

_handleBookTappedтакже необходимо использовать методnotifyListenersзаменятьsetState:

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

После открытия новой страницы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 Внедрение пользовательских анимаций маршрутизации.

Во-первых, чтобыNavigatorпройти в обычае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;
  }
}

здесь,NoAnimationTransitionDelegateпереписанныйTransitionDelegateизresolve()метод, который может указать, будут ли отображаться различные анимации перехода на страницу маршрутизации.Существует несколько следующих методов:

  • markForPush, использовать анимацию перехода при открытии страницы.

  • markForAdd, без использования анимации перехода при открытии страницы.

  • markForPop, используйте анимацию при появлении страницы и уведомляйте приложение о том, что событие передаетсяAppRouterDelegateизonPopPageфункция.

  • markForComplete, анимация перехода не используется при открытии страницы, и приложение также уведомляется.

  • markForRemove, анимация перехода не используется, когда страница всплывает, и приложение не уведомляется.

Этот класс влияет только на последний декларативный API, поэтому кнопка «Назад» по-прежнему показывает анимацию перехода.

Общий процесс таков: здесь просматриваются все объявленные страницы, включая новые страницы (newPageRouteHistory) и существующие страницы (locationToExitingPageRoute), отметьте их, используя несколько из вышеперечисленных способов. использовать на новой страницеmarkForAddубрать анимацию открытого перехода,locationToExitingPageRouteиспользовать вmarkForRemoveАнимации перехода не отображаются, а приложения не уведомляются.Посмотреть полный пример.

дальнейшее чтение

Flutter Navigator2.0 Полное руководство и принципиальный анализ

Изучение новой системы навигации и маршрутизации Flutter

Смотреть! Flutter 1.22 официально выпущен

Три важных дерева во Flutter (процесс рендеринга, ограничения компоновки и т. д.)

GitHub.com/flutter/Appendix…

Оригинальный текст моего WeChat:Tickets.WeChat.QQ.com/Yes/F U9EF HJ4X…

Добро пожаловать на обмен и учиться со мной.