Skip to content

Instantly share code, notes, and snippets.

@johnpryan
Last active Sep 15, 2021
Embed
What would you like to do?
TransitionDelegate example
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);
if (uri.pathSegments.length >= 2) {
var remaining = uri.pathSegments[1];
return BookRoutePath.details(int.tryParse(remaining));
} else {
return BookRoutePath.home();
}
}
@override
RouteInformation restoreRouteInformation(BookRoutePath path) {
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;
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 => _selectedBook == null
? BookRoutePath.home()
: BookRoutePath.details(books.indexOf(_selectedBook));
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
transitionDelegate: NoAnimationTransitionDelegate(),
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
_selectedBook = null;
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath path) async {
if (path.isDetailsPage) {
_selectedBook = books[path.id];
}
}
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;
BookRoutePath.home() : id = null;
BookRoutePath.details(this.id);
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 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;
}
}
@XinyueZ

This comment has been minimized.

Copy link

@XinyueZ XinyueZ commented Nov 12, 2020

2.0 makes thing complex.

@namphho

This comment has been minimized.

Copy link

@namphho namphho commented Nov 17, 2020

an example is so complex

@astnt

This comment has been minimized.

Copy link

@astnt astnt commented Nov 22, 2020

It's not easy to understand but declarative approach is much better.

@venkatd

This comment has been minimized.

Copy link

@venkatd venkatd commented Nov 26, 2020

Yes I agree the API can be simplified quite a bit, but once I got used to the API, I found that a lot of edge cases became easier to deal with. Our new routing setup is more robust and easier to customize than the one before.

Hopefully someone writes an easier to use API layer on top of it all

@just-kip

This comment has been minimized.

Copy link

@just-kip just-kip commented Nov 27, 2020

Is there a way to get a result from details page after pop back?

@mikeesouth

This comment has been minimized.

Copy link

@mikeesouth mikeesouth commented Jan 2, 2021

I'm curious how a login screen would fit in here. I want to have a login screen with one layout and after login I want to have several pages after login. Should I present the login screen as a traditional widget, use the traditional Navigator.push() to the "portal" and then within the portal I use the new router or should I set it up so that even the login screen is included in the new router? There is an example of nested router in the medium article but I do not think that its the way to go since they aren't nested not sure if I should use that for a single page (Login) and the rested in the sub router.

I would really like more examples of this router. Does anyone have public github repos of this?

@victormhg

This comment has been minimized.

Copy link

@victormhg victormhg commented Jan 26, 2021

I'm curious how a login screen would fit in here. I want to have a login screen with one layout and after login I want to have several pages after login. Should I present the login screen as a traditional widget, use the traditional Navigator.push() to the "portal" and then within the portal I use the new router or should I set it up so that even the login screen is included in the new router? There is an example of nested router in the medium article but I do not think that its the way to go since they aren't nested not sure if I should use that for a single page (Login) and the rested in the sub router.

I would really like more examples of this router. Does anyone have public github repos of this?

@mikeesouth, maybe this is the example you are looking for: https://github.com/dleurs/Flutter-Navigator-2.0-demo-with-Authentication-mecanism

@chinitadelrey

This comment has been minimized.

Copy link

@chinitadelrey chinitadelrey commented Jan 26, 2021

@victormhg, it seems the issue with that is that it brings in bloc?

I am interested in the same question as @mikeesouth, but do want to keep it simple -- not load on additional frameworks.

@thanhnamitit

This comment has been minimized.

Copy link

@thanhnamitit thanhnamitit commented Mar 19, 2021

Is there a way to get a result from details page after pop back?

I have same question!

@xclidongbo

This comment has been minimized.

Copy link

@xclidongbo xclidongbo commented Jun 22, 2021

2.0? no no no, just turn on 3.0.

@nagringapp

This comment has been minimized.

Copy link

@nagringapp nagringapp commented Aug 11, 2021

I'm curious how a login screen would fit in here. I want to have a login screen with one layout and after login I want to have several pages after login. Should I present the login screen as a traditional widget, use the traditional Navigator.push() to the "portal" and then within the portal I use the new router or should I set it up so that even the login screen is included in the new router? There is an example of nested router in the medium article but I do not think that its the way to go since they aren't nested not sure if I should use that for a single page (Login) and the rested in the sub router.

I would really like more examples of this router. Does anyone have public github repos of this?

I see you comment was 8 months ago, can't find any decent example with a more complex navigation, would you have some sample to share?

@ezone-dev

This comment has been minimized.

Copy link

@ezone-dev ezone-dev commented Aug 23, 2021

Using this main.dart in a fresh flutter 2.2.3 project to kick the tyres and it doesn't run which isn't filling me with confidence.

@MainTobias

This comment has been minimized.

Copy link

@MainTobias MainTobias commented Aug 26, 2021

Yeah you have to make it compliant with null safety:

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<StatefulWidget> createState() => _BooksAppState();
}

class _BooksAppState extends State<BooksApp> {
  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<BookRoutePath> {
  @override
  Future<BookRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.location!);

    if (uri.pathSegments.length >= 2) {
      var remaining = uri.pathSegments[1];
      return BookRoutePath.details(int.tryParse(remaining));
    } else {
      return 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<BookRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
  @override
  final GlobalKey<NavigatorState> navigatorKey;

  Book? _selectedBook;

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

  @override
  BookRoutePath get currentConfiguration => _selectedBook == null
      ? BookRoutePath.home()
      : BookRoutePath.details(books.indexOf(_selectedBook!));

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,

      pages: [
        MaterialPage(
          key: const 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
        _selectedBook = null;
        notifyListeners();

        return true;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(BookRoutePath configuration) async {
    if (configuration.isDetailsPage) {
      _selectedBook = books[configuration.id!];
    }
  }

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

class BookDetailsPage extends Page {
  final Book? book;

  BookDetailsPage({
    this.book,
  }) : super(key: ValueKey(book));

  @override
  Route createRoute(BuildContext context) {
    return MaterialPageRoute(
      settings: this,
      builder: (BuildContext 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<Book> books;
  final ValueChanged<Book> onTapped;

  const BooksListScreen({Key? key,
    required this.books,
    required this.onTapped,
  }) : 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({Key? key,
    required this.book,
  }) : 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: [
            if (book != null) ...[
              Text(book!.title, style: Theme.of(context).textTheme.headline6),
              Text(book!.author, style: Theme.of(context).textTheme.subtitle1),
            ],
          ],
        ),
      ),
    );
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment