Skip to content

Instantly share code, notes, and snippets.

@johnpryan
Last active January 4, 2024 03:30
Show Gist options
  • Star 37 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save johnpryan/5ce79aee5b5f83cfababa97c9cf0a204 to your computer and use it in GitHub Desktop.
Save johnpryan/5ce79aee5b5f83cfababa97c9cf0a204 to your computer and use it in GitHub Desktop.
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
Copy link

XinyueZ commented Nov 12, 2020

2.0 makes thing complex.

@namphho
Copy link

namphho commented Nov 17, 2020

an example is so complex

@astnt
Copy link

astnt commented Nov 22, 2020

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

@venkatd
Copy link

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
Copy link

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

@mikeesouth
Copy link

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
Copy link

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
Copy link

@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
Copy link

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

I have same question!

@xclidongbo
Copy link

2.0? no no no, just turn on 3.0.

@nagringapp
Copy link

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
Copy link

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
Copy link

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

@radzish
Copy link

radzish commented Aug 12, 2022

Hello, even though this thread is quite old, I hope that someone will help me with some questions / issues.

I am very happy that example is simple, however I see some issues with it mostly related to WEB implementation. I am trying to polish it to the level it behaves as consistent with native browser navigation as possible.

Example's main issue is with browser history: every time user presses back arrow it returns it to home screen, but instead of moving back in history, it just pushes new entry, so history is constantly growing (I belive that "back arrow" button executes Navigator.of(context).pop() under the hood). Is that possible to make navigator's pop behavior the same as of browser's back button? I have solution for that:

html.window.history.go(-1);

but I am not sure it is the best one.

Second question is about page title (what we can see in tab and what we can see in browser history, like Application: Home for home or Application: Book Title for book details page).
I tried using Title widget, but I am not sure where I should put it in our example to make it work.

thanks a lot and looking forward some response from you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment