riverpod_navigator 0.10.0 riverpod_navigator: ^0.10.0 copied to clipboard
Simple but powerfull Flutter navigation with riverpod, freezed and Navigator 2.0.
Navigator for Riverpod #
Simple but powerfull navigation library (based on Flutter Navigator 2.0, Riverpod, and Freezed) that solves the following problems: #
- Strictly typed navigation:
You can usenavigate([Home(), Books(), Book(id: bookId)]);
instead ofnavigate('home/books/$bookId');
in your code. - Easier coding:
The problem of navigation is reduced to manipulation an immutable collection. - Better separation of concerns: UI x Model (thanks to riverpod 👍):
Navigation logic can be developed and tested without typing a single flutter widget. - Asynchronous navigation:
Before starting navigation, prepare all necessary asynchronous operations, e.g.- loading data for new screen
- save data from the previous screen
- Dependence on external providers:
The navigation state may also depend on external providers, e.g. on login status - Possibility to configure many navigation parameters
The mission #
Take a look at the following terms:
- string-path:
stringPath = 'home/books/book;id=2';
- string-segment - the string-path consists of three string-segments: 'home', 'books', 'book;id=2'
- typed-path:
typedPath = [HomeSegment(), BooksSegment(), BookSegment(id:2)];
- typed-segment - the typed-path consists of three typed-segments: [HomeSegment], [BooksSegment], [BookSegment]
- navigation-stack of Flutter Navigator 2.0:
HomeScreen(HomeSegment())) => BooksScreen(BooksSegment()) => BookScreen(BookSegment(id:3))
The mission of navigation is to keep string-path <= typed-path => navigation-stack always in sync. With the typed-path as the source of the truth.
How does it work #
If anyone wants to understand how the riverpod_navigator package works, let them look at riverpod_navigator_example. It validates the idea of collaboration Riverpod + Freezed + Flutter Navigator 2.0.
Comparison with go_router #
This chapter is inspired by this riverpod issue: Examples of go_router using riverpod.
example | go_router | code lines | riverpod_navigator | code lines |
---|---|---|---|---|
main | source code | 70 | source code | 84 |
redirection | source code | 167 | source code | 149 |
If you are interested in preparing another go_router example, I will try to do it.
How to use it #
The best documentation is a simple source code.
See an example of the classic Home => Books => Book*
application in Lesson01 ... Lesson05 below.
Lesson01 #
(whole example see at lesson01.dart source code)
1. define classes for typed-segments (aka TypedSegment) #
From the following AppSegments class declaration, the freezed package generates three typed-segment classes: HomeSegment, BooksSegment and BookSegment.
@freezed
class AppSegments with _$AppSegments, TypedSegment {
AppSegments._();
factory AppSegments.home() = HomeSegment;
factory AppSegments.books() = BooksSegment;
factory AppSegments.book({required int id}) = BookSegment;
factory AppSegments.fromJson(Map<String, dynamic> json) => _$AppSegmentsFromJson(json);
}
2. Type App-specific navigator (aka AppNavigator) #
AppNavigator is a singleton class that does the following:
- configures various navigation parameters
- contains actions related to navigation. The actions are then used in the screen widgets.
2.1. Navigation parameters
class AppNavigator extends RiverpodNavigator {
AppNavigator(Ref ref)
: super(
ref,
// home (initial) navigation path
initPath: [HomeSegment()],
// how to decode JSON to TypedSegment
json2Segment: (jsonMap, _) => AppSegments.fromJson(jsonMap),
// map TypedSegment's to navigation-stack Screens
screenBuilder: appSegmentsScreenBuilder,
);
2.2. Common navigation actions
//
Future<void> toHome() => navigate([HomeSegment()]);
Future<void> toBooks() => navigate([HomeSegment(), BooksSegment()]);
Future<void> toBook({required int id}) => navigate([HomeSegment(), BooksSegment(), BookSegment(id: id)]);
Future<void> bookNextPrevButton({bool? isPrev}) {
assert(currentTypedPath.last is BookSegment);
var id = (currentTypedPath.last as BookSegment).id;
if (isPrev == true)
id = id == 0 ? booksLen - 1 : id - 1;
else
id = booksLen - 1 > id ? id + 1 : 0;
return toBook(id: id);
}
3. Root widget #
Note: To make it less verbose, we use the functional_widget package to generate widgets. See generated "lesson0?.g.dart"" file for details.
@cwidget
Widget booksExampleApp(WidgetRef ref) {
final navigator = ref.read(riverpodNavigatorProvider);
return MaterialApp.router(
title: 'Books App',
routerDelegate: navigator.routerDelegate,
routeInformationParser: RouteInformationParserImpl(navigator.pathParser),
debugShowCheckedModeBanner: false,
);
}
4. App entry point #
app entry point with ProviderScope.overrides
void runMain() => runApp(
ProviderScope(
overrides: [
riverpodNavigatorCreatorProvider.overrideWithValue(AppNavigator.new /*See Constructor tear-offs in Dart ^2.15*/),
],
child: const BooksExampleApp(),
),
);
5. Map TypedSegment's to Screens #
You can view all application screens and widgets here: screen.dart source code
final ScreenBuilder appSegmentsScreenBuilder = (segment) => (segment as AppSegments).map(
// See Constructor tear-offs in Dart ^2.15, "HomeScreen.new" is equivalent to "(segment) => HomeScreen(segment)"
home: HomeScreen.new,
books: BooksScreen.new,
book: BookScreen.new,
);
Other lessons: #
Lesson02 #
Lesson02 is lesson01 enhanced with:
- asynchronous navigation when screens require some asynchronous actions (when creating, deactivating, or merging)
- the splash screen appears before the HomeScreen is displayed
Lesson03 #
Lesson03 is lesson02 extended by:
- login application logic (where some pages are not available without a logged in user)
- more TypedPath roots (AppSegments and LoginSegments)
- navigation state also depends on another provider (userIsLoggedProvider)
Lesson04 #
Lesson04 is lesson03 prepared using the router concept.
Lesson05 #
Lesson05 is the same as lesson03 but without screens and widgets. It has not any GUI, only a test.
Doc TODO #
In this case, it is an advanced parameterization. Applies to the RouterDelegate.build method.
It is possible to parameterize the values navigator.screen2Page and navigator.navigatorWidgetBuilder below:
@override
Widget build(BuildContext context) {
final actPath = currentConfiguration;
if (actPath.isEmpty) return navigator.splashBuilder?.call() ?? SizedBox();
final navigatorWidget = Navigator(
key: navigatorKey,
// segment => screen
pages: actPath.map((segment) => navigator.screen2Page!(segment, navigator.screenBuilder!)).toList(),
onPopPage: (route, result) {
//if (!route.didPop(result)) return false;
// remove last segment from path
navigator.onPopRoute();
return false;
});
return navigator.navigatorWidgetBuilder == null ? navigatorWidget : navigator.navigatorWidgetBuilder!(context, navigatorWidget);
}
Roadmap #
I prepared the package for my new project. Its further development depends on whether it will be used by the community.
- proofreading because my English is not good. Community help is warmly welcomed.
- testing on mobile (tested so far for windows desktop and web)
Navigator.onPopPage may need improvements. - nested navigation flow
I think everything is ready, nested ProviderScope can solve nested navigation too. - BlockGUI widget (block the GUI while asynchronous navigation is waiting to complete)
- parameterization alowing cupertino