riverpod_navigator 0.29.0 riverpod_navigator: ^0.29.0 copied to clipboard
Simple but powerfull Flutter navigation with riverpod, freezed and Navigator 2.0.
Riverpod navigation #
Simple but powerful Flutter navigation with riverpod, freezed and Navigator 2.0 that solves the following: #
- Strictly typed navigation:
you can usenavigate([HomeSegment(),BookSegment(id: 2)]);
instead ofnavigate('home/book;id:2');
in your code - asynchronous navigation
is the case when changing the navigation state requires asynchronous actions (such as loading or saving data from the Internet) - multiple providers
is the case when the navigation state depends on multiple riverpod providers - 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 - nested navigation
just use the nested riverpodProviderScope()
and FlutterRouter
widget
Terminology used #
Take a look at the following terms related to URL path home/book;id=2
- string-path:
final stringPath = 'home/book;id=2';
- string-segment: the string-path consists of two slash-delimited string-segments (
home
andbook;id=2
) - typed-segment: (
class TypedSegment {}
's descendant) describes coresponding string-segment's (HomeSegment()
andBookSegment(id:2)
) - typed-path: (
typedef TypedPath = List<TypedSegment>
) describes coresponding string-path ([HomeSegment(), BookSegment(id:2)];
) - Flutter Navigator 2.0 navigation-stack is uniquely determined by the TypedPath (where each TypedSegment instance corresponds to a screen and page instance):
[MaterialPage (child: HomeScreen(HomeSegment())), MaterialPage (child: BookScreen(BookSegment(id:2)))]
Simple example #
Create an application using these simple steps:
Step1 - define classes for the typed-segment #
Note: fromUrlPars and toUrlPars helps to convert typed-segment to string-segment and back.
class HomeSegment extends TypedSegment {
const HomeSegment();
factory HomeSegment.fromUrlPars(UrlPars pars) => const HomeSegment();
}
class PageSegment extends TypedSegment {
const PageSegment({required this.title});
factory PageSegment.fromUrlPars(UrlPars pars) => PageSegment(title: map.getString('title'));
@override
void toUrlPars(UrlPars pars) => map.setString('title', title);
final String title;
}
Step2 - configure AppNavigator... #
by extending the RNavigator class:
class AppNavigator extends RNavigator {
AppNavigator(Ref ref)
: super(
ref,
[
RRoute<HomeSegment>(HomeSegment.fromUrlPars, HomeScreen.new), // build a HomeScreen for HomeSegment
RRoute<PageSegment>(PageSegment.fromUrlPars, PageScreen.new), // build a PageScreen for PageSegment
],
);
}
Step3 - use the AppNavigator in MaterialApp.router #
If you are familiar with the Flutter Navigator 2.0 and the riverpod, the following code is clear:
class App extends ConsumerWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final navigator = ref.read(navigatorProvider) as AppNavigator;
return MaterialApp.router(
title: 'Riverpod Navigator Example',
routerDelegate: navigator.routerDelegate,
routeInformationParser: navigator.routeInformationParser,
debugShowCheckedModeBanner: false,
);
}
}
Step4 - place and configure riverpod ProviderScope ... #
... in main entry point
void main() => runApp(
ProviderScope(
// home-path and navigator constructor are required
overrides: RNavigatorCore.providerOverrides([HomeSegment()], AppNavigator.new),
child: const App(),
),
);
And that's all #
Navigation to a specific screen is performed as follows:
// navigation to PageScreen
ElevatedButton(
onPressed: () => ref.read(navigatorProvider).navigate([HomeSegment(), PageSegment(title: 'Page')]),
// navigation to HomeScreen
ElevatedButton(
onPressed: () => ref.read(navigatorProvider).navigate([HomeSegment()]),
Running applications, source code and test, see:
Testing #
Before developing a GUI, it is good practice to develop and test the invisible part of the application (app model and state). It is recommended to use a dart test environment, see:
test('navigation test', () async {
final container = ProviderContainer(overrides: RNavigatorCore.providerOverrides([HomeSegment()], AppNavigator.new));
final navigator = container.read(navigatorProvider);
Future navigTest(Future action(), String expected) async {
await action();
await container.pump();
expect(navigator.navigationStack2Url, expected);
}
await navigTest(() => navigator.navigate([HomeSegment()]), 'home');
await navigTest(() => navigator.navigate([HomeSegment(), PageSegment(title: 'Page')]), 'home/page;title=Page');
await navigTest(() => navigator.pop(), 'home');
await navigTest(() => navigator.push(PageSegment(title: 'Page2')), 'home/page;title=Page2');
await navigTest(() => navigator.replaceLast<PageSegment>((old) => PageSegment(title: 'X${old.title}')), 'home/page;title=XPage2');
});
Other features and examples #
Installation of examples #
After cloning the riverpod_navigator repository, go to examples/doc
subdirectory and execute:
flutter create .
flutter pub get
See the /lib subdirectory for examples.
Navigator Data Flow Diagram: #
As you can see, changing the Input state starts the async calculation. The result of the calculations is Output state which can have app-specific Side effects. Navigator 2.0 RouterDelegate is then synchronized with navigationStackProvider
Roadmap #
I prepared this package for my new project. Its further development depends on whether the community will use it.
- proofreading because my English is not good. Community help is warmly welcomed.
- parameterization allowing Cupertino