Navigator for Riverpod
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 of the immutable collection. - Better separation of concerns: UI x Model (thanks to riverpod :+1:):
Navigation logic can be developed and tested in the Dart environment, without typing a single flutter widget. - Small codebase with a lot of extensions:
The core engine consists of two small .dart files (riverpod_navigator.dart and riverpod_navigator_dart.dart). Additional features (such as better URL parser, asynchronous navigation, possibility to use routes etc.) are included (and can be created) as configurable extensions.
Two packages
The "Riverpod navigator" consists of two packages, similar to a "riverpod". The following table explains its similarity:
Dart only development and testing | Flutter development and testing |
---|---|
riverpod | flutter_riverpod or hooks_riverpod |
riverpod_navigator_dart | riverpod_navigator |
Install and run examples
After clonning repository, go to examples/doc/
subdirectory and execute:
flutter create .
flutter pub get
flutter pub run build_runner --delete-conflicting-outputs
- in lib/main.dart), uncomment the line you want to execute.
- execute
flutter run
Explanation on examples
For a better understanding, everything is explained on the classic 3-screens example:
Home
=> Books
=> Book\*
Examples index
- Lesson01: simple example
- Lesson02: example with Dart testing
- Lesson03: asynchronous navigation
- Lesson03.1: splash screen
- Lesson04: app logic, more TypedSegment classes per app
- Lesson05: using the Route concept
- Lesson06: splash screen
- Lesson07: waiting indicator, navigatorWidgetBuilder
- Lesson08: screenBuilder
Lesson01: simple example
Example file is available here: lesson01.dart
1. Classes for typed "url path segments" (TypedSegment)
The Freezed package generates three immutable classes used for writing typed navigation path, e.g
TypedPath newPath = <TypedSegment>[HomeSegment (), BooksSegment (), BookSegment (id: 3)];
@freezed
class AppSegments with _$AppSegments, TypedSegment {
AppSegments._();
factory AppSegments.home() = HomeSegment;
factory AppSegments.books() = BooksSegment;
factory AppSegments.book({required int id}) = BookSegment;
factory AppSegments.splash() = SplashSegment;
factory AppSegments.fromJson(Map<String, dynamic> json) => _$AppSegmentsFromJson(json);
}
2. Dart part of app configuration
Tell the application how to convert segments from JSON.
final config4DartCreator = () => Config4Dart(initPath: [HomeSegment()], json2Segment: (json, _) => AppSegments.fromJson(json));
3. app specific navigator with navigation aware actions
... actions are then used in app widgets.
const booksLen = 5;
class AppNavigator extends RiverpodNavigator {
AppNavigator(Ref ref, Config4Dart config) : super(ref, config);
/// navigate to home page
void toHome() => navigate([HomeSegment()]);
/// navigate to books page
void toBooks() => navigate([HomeSegment(), BooksSegment()]);
/// navigate to book;id=3 page
void toBook({required int id}) => navigate([HomeSegment(), BooksSegment(), BookSegment(id: id)]);
/// cyclic book's navigation (Prev and Next buttons)
void bookNextPrevButton({bool? isPrev}) {
assert(getActualTypedPath().last is BookSegment);
var id = (getActualTypedPath().last as BookSegment).id;
if (isPrev == true)
id = id == 0 ? booksLen - 1 : id - 1;
else
id = booksLen - 1 > id ? id + 1 : 0;
toBook(id: id);
}
}
4. providers
final appNavigatorProvider =
Provider<AppNavigator>((ref) => AppNavigator(ref, ref.watch(config4DartProvider)));
/// Provider for Flutter 2.0 RouterDelegate
final appRouterDelegateProvider =
Provider<RiverpodRouterDelegate>((ref) =>
RiverpodRouterDelegate(ref, ref.watch(configProvider), ref.watch(appNavigatorProvider)));
5. Flutter-part of app configuration
final configCreator = (Config4Dart config4Dart) => Config(
/// Which widget will be builded for which [TypedSegment].
/// Used in [RiverpodRouterDelegate] to build pages from [TypedSegment]'s
screenBuilder: (segment) => (segment as AppSegments).map(
home: (home) => HomeScreen(home),
books: (books) => BooksScreen(books),
book: (book) => BookScreen(book),
),
config4Dart: config4Dart,
);
6. root widget for app
Using functional_widget package to be less verbose. Package generates "class BooksExampleApp extends ConsumerWidget...", see *.g.dart. "functional_widget" is not a mandatory app dependency.
@cwidget
Widget booksExampleApp(WidgetRef ref) => MaterialApp.router(
title: 'Books App',
routerDelegate: ref.watch(appRouterDelegateProvider),
routeInformationParser: RouteInformationParserImpl(ref.watch(config4DartProvider)),
);
7. app entry point...
... with ProviderScope and ProviderScope.overrides
void main() {
runApp(ProviderScope(
// initialize providers with the configurations defined above
overrides: [
config4DartProvider.overrideWithValue(config4DartCreator()),
configProvider.overrideWithValue(configCreator(config4DartCreator())),
],
child: const BooksExampleApp(),
));
}
8. app screens
File with screen widgets is available here: screens.dart
Lesson02: example with Dart testing
An example that allows flutter-independent testing.
to be done
Lesson03: asynchronous navigation
See changes against Example01 (added part 1.1, parts 2. and 3. are modified).
Example file is available here: lesson03.dart.
1.1 async screen actions
Add new code for async screen actions.
AsyncScreenActions? segment2AsyncScreenActions(TypedSegment segment) {
Future<String> simulateAsyncResult(String title, int msec) async {
await Future.delayed(Duration(milliseconds: msec));
return title;
}
return (segment as AppSegments).maybeMap(
book: (_) => AsyncScreenActions<BookSegment>(
// for every Book screen: creating takes some time
creating: (newSegment) async => simulateAsyncResult('Book creating async result after 1 sec', 1000),
// for every Book screen with odd id: changing to another Book screen takes some time
merging: (_, newSegment) async =>
newSegment.id.isOdd ? simulateAsyncResult('Book merging async result after 500 msec', 500) : null,
// for every Book screen with even id: creating takes some time
deactivating: (oldSegment) =>
oldSegment.id.isEven ? Future.delayed(Duration(milliseconds: 500)) : null,
),
home: (_) => AsyncScreenActions<HomeSegment>(
// Home screen takes some timefor creating
creating: (_) async => simulateAsyncResult('Home creating async result after 1 sec', 1000)),
orElse: () => null,
);
}}
2. Configure dart-part of app
Add segment2AsyncScreenActions
to config
final config4DartCreator = () => Config4Dart(
json2Segment: (json, _) => AppSegments.fromJson(json),
initPath: [HomeSegment()],
segment2AsyncScreenActions: segment2AsyncScreenActions,
);
3. app-specific navigator with navigation aware actions
AppNavigator extends AsyncRiverpodNavigator instead of RiverpodNavigator
class AppNavigator extends AsyncRiverpodNavigator {
...
Lesson03.1: splash screen
1. Classes for typed "url path segments" (TypedSegment)
Add SplashSegment definition
@freezed
class AppSegments with _$AppSegments, TypedSegment {
AppSegments._();
factory AppSegments.home() = HomeSegment;
factory AppSegments.books() = BooksSegment;
factory AppSegments.book({required int id}) = BookSegment;
factory AppSegments.splash() = SplashSegment; // <========
factory AppSegments.fromJson(Map<String, dynamic> json) => _$AppSegmentsFromJson(json);
}
2. Dart part of app configuration
Specify splashPath.
final config4DartCreator = () => Config4Dart(
json2Segment: (json, _) => AppSegments.fromJson(json),
segment2AsyncScreenActions: segment2AsyncScreenActions,
initPath: [HomeSegment()],
splashPath: [SplashSegment()], // <========
);
5. Flutter-part of app configuration
Specify splash screen builder.
final configCreator = (Config4Dart config4Dart) => Config(
/// Which widget will be builded for which [TypedSegment].
/// Used in [RiverpodRouterDelegate] to build pages from [TypedSegment]'s
screenBuilder: (segment) => (segment as AppSegments).map(
home: (s) => HomeScreen(s),
books: (s) => BooksScreen(s),
book: (s) => BookScreen(s),
splash: (s) => SplashScreen(s), // <========
),
config4Dart: config4Dart,
);
8. app screens
Add SplashScreen widget to screens.dart
Lesson04: app logic, more TypedSegment classes per app
Simple Login navigation flow
to be done
Lesson05: using the Route concept
to be done
Lesson06: splash screen
to be done
Lesson07: waiting indicator, navigatorWidgetBuilder
to be done
Lesson08: screenBuilder
to be done