// Copyright 2020, the Flutter project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. /// Shows how a custom TransitionDelegate can be used to customized when /// transition animations are shown. (For example, [when two routes are popped /// off the stack](https://github.com/flutter/flutter/issues/12146), however the /// default TransitionDelegate will handle this if you are using Router) import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; void main() { runApp(const BooksApp()); } class Book { final String title; final String author; Book(this.title, this.author); } class BooksApp extends StatefulWidget { const BooksApp({Key? key}) : super(key: key); @override State createState() => _BooksAppState(); } class _BooksAppState extends State { final BookRouterDelegate _routerDelegate = BookRouterDelegate(); final BookRouteInformationParser _routeInformationParser = BookRouteInformationParser(); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'Books App', routerDelegate: _routerDelegate, routeInformationParser: _routeInformationParser, ); } } class BookRouteInformationParser extends RouteInformationParser { @override Future parseRouteInformation( RouteInformation routeInformation, ) { final uri = Uri.parse(routeInformation.location ?? ''); if (uri.pathSegments.length >= 2) { var remaining = uri.pathSegments[1]; return SynchronousFuture(BookRoutePath.details(int.tryParse(remaining)!)); } else { return SynchronousFuture(BookRoutePath.home()); } } @override RouteInformation? restoreRouteInformation(BookRoutePath configuration) { if (configuration.isHomePage) { return const RouteInformation(location: '/'); } if (configuration.isDetailsPage) { return RouteInformation(location: '/book/${configuration.id}'); } return null; } } class BookRouterDelegate extends RouterDelegate with ChangeNotifier, PopNavigatorRouterDelegateMixin { @override final GlobalKey navigatorKey; Book? _selectedBook; final List 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(); @override BookRoutePath get currentConfiguration => _selectedBook == null ? BookRoutePath.home() : BookRoutePath.details(books.indexOf(_selectedBook!)); @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, transitionDelegate: NoAnimationTransitionDelegate(), pages: [ MaterialPage( key: const ValueKey('BooksListPage'), child: BooksListScreen( books: books, onTapped: _handleBookTapped, ), ), if (_selectedBook != null) BookDetailsPage(book: _selectedBook!) ], onPopPage: (route, dynamic result) { if (!route.didPop(result)) { return false; } // Update the list of pages by setting _selectedBook to null _selectedBook = null; notifyListeners(); return true; }, ); } @override Future setNewRoutePath(BookRoutePath configuration) { if (configuration.isDetailsPage) { _selectedBook = books[configuration.id!]; } return SynchronousFuture(null); } void _handleBookTapped(Book book) { _selectedBook = book; notifyListeners(); } } class BookDetailsPage extends Page { final Book book; BookDetailsPage({ required this.book, }) : super(key: ValueKey(book)); @override Route createRoute(BuildContext context) { return MaterialPageRoute( settings: this, builder: (context) { return BookDetailsScreen(book: book); }, ); } } class BookRoutePath { final int? id; BookRoutePath.home() : id = null; BookRoutePath.details(this.id); bool get isHomePage => id == null; bool get isDetailsPage => id != null; } class BooksListScreen extends StatelessWidget { final List books; final ValueChanged onTapped; const BooksListScreen({ required this.books, required this.onTapped, Key? key, }) : super(key: key); @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; const BookDetailsScreen({ required this.book, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(book.title, style: Theme.of(context).textTheme.headline6), Text(book.author, style: Theme.of(context).textTheme.subtitle1), ], ), ), ); } } class NoAnimationTransitionDelegate extends TransitionDelegate { @override Iterable resolve({ required List newPageRouteHistory, required Map locationToExitingPageRoute, required Map> pageRouteToPagelessRoutes, }) { final results = []; 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; } }