Simple but powerfull Flutter navigation with riverpod and Navigator 2.0

Overview

Riverpod navigation

If you are interested in the motivation why the package was created and a detailed description of what problems it solves, read this MEDIUM article: Simple Flutter navigation with Riverpod.

Simple but powerful Flutter navigation with riverpod and Navigator 2.0 that solves the following:

  • Strictly typed navigation:
    you can use navigate([HomeSegment(),BookSegment(id: 2)]); instead of navigate('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 navigation problem is reduced to manipulating the class 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 riverpod ProviderScope() and Flutter Router widget

Index

Terminology used

Take a look at the following terms related to URL path home/book;id=2

  • string-path: e.g. home/book;id=2
  • string-segment: the string-path consists of two slash-delimited string-segments (home and book;id=2)
  • typed-segment describes coresponding string-segments (HomeSegment() for 'home' and BookSegment(id:2) for 'book;id=2')
    typed-segment is class TypedSegment {}'s descendant.
  • typed-path describes coresponding string-path ([HomeSegment(), BookSegment(id:2)])
    typed-path is typedef TypedPath = List
  • Flutter Navigator 2.0 navigation-stack is uniquely determined by the TypedPath (where each TypedPath's TypedSegment instance corresponds to a screen and page instance):
    pages = [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

class HomeSegment extends TypedSegment {
  const HomeSegment();
  factory HomeSegment.decode(UrlPars pars) => const HomeSegment();
}

class BookSegment extends TypedSegment {
  const BookSegment({required this.id});
  factory BookSegment.decode(UrlPars pars) => BookSegment(id: pars.getInt('id'));

  final int id;
  @override
  void encode(UrlPars pars) => pars.setInt('id', id);
}

encode and decncode helps to convert typed-segment to string-segment and back.

Step2 - configure AppNavigator...

... by extending the RNavigator class.

class AppNavigator extends RNavigator {
  AppNavigator(Ref ref)
      : super(
          ref,
          [
            /// 'home' and 'book' strings are used in web URL, e.g. 'home/book;id=2'
            /// decode is used to decode URL to HomeSegment/BookSegment
            /// HomeScreen/BookScreen.new are screen builders for a given segment
            RRoute<HomeSegment>(
              'home',
              HomeSegment.decode,
              HomeScreen.new,
            ),
            RRoute<BookSegment>(
              'book',
              BookSegment.decode,
              BookScreen.new,
            ),
          ],
        );
}

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

Step4 - configure riverpod ProviderScope ...

... in main entry point

void main() => runApp(
      ProviderScope(
        // [HomeSegment()] as home TypedPath and navigator constructor are required
        overrides: providerOverrides([HomeSegment()], AppNavigator.new),
        child: const App(),
      ),
    );

Step5 - code screen widgets

There are two screen to code: HomeScreen and BookScreen. Extend this screens from RScreen widget.

class BookScreen extends RScreen<AppNavigator, BookSegment> {
  const BookScreen(BookSegment segment) : super(segment);

  @override
  Widget buildScreen(ref, navigator, appBarLeading) => Scaffold(
        appBar: AppBar(
          title: Text('Book ${segment.id}'),
          /// [appBarLeading] overrides standard back button behavior
          leading: appBarLeading,
        ),
        body: 
...

RScreen widget:

  • replaces the standard Android back button behavior (using Flutter BackButtonListener widget)
  • will provide appBarLeading icon to replace the standard AppBar back button behavior

This is essential for asynchronous navigation to function properly.

And that's all

See:

The link Go to book: [3, 13, 103] in the running example would not make much sense in the real Books application. But it shows the navigation to the four-screen navigation stack:

  • string-path = home/book;id=3/book;id=13/book;id=103.
  • typed-path = [HomeSegment(), BookSegment(id:3), BookSegment(id:13), BookSegment(id:103)].
  • navigation-stack (flutter Navigator.pages) = [MaterialPage (child: HomeScreen(HomeSegment())), MaterialPage (child: BookScreen(BookSegment(id:3))), MaterialPage (child: BookScreen(BookSegment(id:13))), MaterialPage (child: BookScreen(BookSegment(id:103)))].

Development and testing without GUI

Navigation logic can be developed and tested without typing a single flutter widget:

  test('navigation model', () async {
    final container = ProviderContainer(
      overrides: 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(), BookSegment(id: 1)]),
      'home/book;id=1',
    );
    await navigTest(
      () => navigator.pop(),
      'home',
    );
    await navigTest(
      () => navigator.push(BookSegment(id: 2)),
      'home/book;id=2',
    );
    await navigTest(
      () => navigator.replaceLast<BookSegment>((old) => BookSegment(id: old.id + 1)),
      'home/book;id=3',
    );
  });

URL parsing

Flutter Navigator 2.0 and its MaterialApp.router constructor requires a URL parser (RouteInformationParser). We use URL syntax, see section 3.3. of RFC 3986, note *For example, one URI producer might use a segment such as "name;v=1.1"..."

Each TypedSegment must be converted to string-segment and back. The format of string-segment is

[;=]*, e.g. book;id=3.

encode/decode example:

Instead of directly converting to/from the string, we convert to/from
typedef UrlPars = Map

So far, we support the following types of TypedSegment property:
int, double, bool, String, int?, double?, bool?, String?.

class TestSegment extends TypedSegment {
  const TestSegment({required this.i, this.s, required this.b, this.d});

  factory TestSegment.decode(UrlPars pars) => TestSegment(
        i: pars.getInt('i'),
        s: pars.getStringNull('s'),
        b: pars.getBool('b'),
        d: pars.getDoubleNull('d'),
      );

  @override
  void encode(UrlPars pars) => 
    pars.setInt('i', i).setString('s', s).setBool('b', b).setDouble('d', d);

  final int i;
  final String? s;
  final bool b;
  final double? d;
}

After registering TestSegment by RRoute('test',TestSegment.decode, the following URL's are correct:

  • test;i=1;b=true
  • test;i=2;b=true;d=12.6;s=abcd
  • test;i=2;b=true/test;i=2;b=true;d=12.6;s=abcd/test;i=3;b=false

Customization

Every aspect of URL conversion can be customized, e.g.

  • support another property type (as a DateTime, providing getDateTime, getDateTimeNull and setDateTime in your own UrlPars's extension)
    See extension UrlParsEx on UrlPars in path_parser.dart.
  • rewrite the entire IPathParser and use a completely different URL syntax. Then use your parser in AppNavigator:
class AppNavigator extends RNavigator {
  AppNavigator(Ref ref)
      : super(
....
  	pathParserCreator: (router) => MyPathParser(router),
...         

Place navigation events in AppNavigator

It is good practice to place the code for all events (specific to navigation) in AppNavigator. These can then be used not only for writing screen widgets, but also for testing.

class AppNavigator extends RNavigator {
  ......
  /// navigate to next book
  Future toNextBook() => replaceLast<BookSegment>((last) => BookSegment(id: last.id + 1));
  /// navigate to home
  Future toHome() => navigate([HomeSegment()]);
}

In the screen widget, it is used as follows:

...
ElevatedButton(
  onPressed: navigator.toNextBook,
  child: Text('Book $id'),
), 
... 

and in the test code as follows:

  await navigTest(navigator.toNextBook, 'home/book;id=3');

Async navigation

Async navigation means that navigation is delayed until the asynchronous actions are performed. These actions for each screen are:

  • opening (before opening a new screen)
  • closing (before closing the old screen)
  • replacing (before replacing the screen with a screen with the same segment type)

The opening and closing actions can return an asynchronous result that can be used later when building a new screen.

Define classes for the typed-segment

Apply a AsyncSegment mixin with appropriate type (String) to TypedSegment's.

class HomeSegment extends TypedSegment with AsyncSegment<String>{
  ....
}

class BookSegment extends TypedSegment  with AsyncSegment<String>{
  ....
}

Configure AppNavigator

Add opening, closing or replacing actions to RRoute definition.

class AppNavigator extends RNavigator {
  AppNavigator(Ref ref)
      : super(
          ref,
          [
            RRoute<HomeSegment>(
              'home',
              HomeSegment.decode,
              HomeScreen.new,
              opening: (sNew) => sNew.setAsyncValue(_simulateAsyncResult('Home.opening', 2000)),
            ),
            RRoute<BookSegment>(
              'book',
              BookSegment.decode,
              BookScreen.new,
              opening: (sNew) => sNew.setAsyncValue(_simulateAsyncResult('Book ${sNew.id}.opening', 240)),
              replacing: (sOld, sNew) => sNew.setAsyncValue(_simulateAsyncResult('Book ${sOld.id}=>${sNew.id}.replacing', 800)),
              closing: (sOld) => Future.delayed(Duration(milliseconds: 500)),
            ),
          ],
        );
....
}

// simulates an action such as saving-to/loading-from external storage
Future<String> _simulateAsyncResult(String asyncResult, int msec) async {
  await Future.delayed(Duration(milliseconds: msec));
  return '$asyncResult: async result after $msec msec';
}

Use the result of an asynchronous action when building the screen

...
Text('Async result: "${segment.asyncValue}"'),
...

See:

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:

riverpod_navigator

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
Comments
  • Review relation between providers and RouteDelegate

    Review relation between providers and RouteDelegate

    Review relation between [RiverpodNavigator] <=> [TypedPathNotifier]<=> [RiverpodRouterDelegate] <=> [ExampleApp]

    see appFlutter.dart

    ...
      final navigator = ref.read(exampleRiverpodNavigatorProvider); 
      final delegate = RiverpodRouterDelegate(navigator, pageBuilder: _pageBuilder, initPath: [HomeSegment()]); 
      ref.listen(typedPathNotifierProvider, (_, __) => delegate.notifyListeners()); 
    ...
    
    help wanted Example 
    opened by PavelPZ 3
  • Error on upgrade to Flutter 3.3.0

    Error on upgrade to Flutter 3.3.0

    Hi again, there's also another issue that I can report when I tried upgrading to Flutter 3.3.0.

    Launching lib/main.dart on SM G955F in debug mode...
    lib/main.dart:1
    : Error: The non-abstract class 'RouteInformationParserImpl' is missing implementations for these members:
    ../…/src/routeDelegate.dart:43
     - RouteInformationParser.parseRouteInformationWithDependencies
    Try to either
    
     - provide an implementation,
     - inherit an implementation from a superclass or mixin,
     - mark the class as abstract, or
     - provide a 'noSuchMethod' implementation.
    
    class RouteInformationParserImpl implements RouteInformationParser<TypedPath> {
          ^^^^^^^^^^^^^^^^^^^^^^^^^^
    : Context: 'RouteInformationParser.parseRouteInformationWithDependencies' is defined here.
    ../…/widgets/router.dart:1212
      Future<T> parseRouteInformationWithDependencies(RouteInformation routeInformation, BuildContext context) {
    
    FAILURE: Build failed with an exception.
    
    * Where:
    Script '/Users/romain/Development/sdk/flutter/packages/flutter_tools/gradle/flutter.gradle' line: 1159
    
    * What went wrong:
    Execution failed for task ':app:compileFlutterBuildDebug'.
    > Process 'command '/Users/romain/Development/sdk/flutter/bin/flutter'' finished with non-zero exit value 1
    
    * Try:
    > Run with --stacktrace option to get the stack trace.
    > Run with --info or --debug option to get more log output.
    > Run with --scan to get full insights.
    
    * Get more help at https://help.gradle.org
    
    BUILD FAILED in 2s
    Exception: Gradle task assembleDebug failed with exit code 1
    Exited
    

    Once again I'm not familiar with this specific error, but reading the logs seems to point out to RouteInformationParserImpl from riverpod_navigator, hence why I report it just in case.

    Thanks!

    bug 
    opened by Roms1383 2
  • Error on startup

    Error on startup "Unhandled Exception: Bad state: No element"

    Hi and thanks for this library, I've started using it a couple of days ago and it saves quite some time. I stumbled upon a weird error on startup though :

    E/flutter ( 5198): [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: Bad state: No element
    E/flutter ( 5198): #0      List.last (dart:core-patch/growable_array.dart:348:5)
    E/flutter ( 5198): #1      RNavigatorCore.navigatePath (package:riverpod_navigator_core/src/navigator.dart:97:30)
    E/flutter ( 5198): #2      RNavigatorCore.navigate (package:riverpod_navigator_core/src/navigator.dart:100:41)
    E/flutter ( 5198): #3      RRouterDelegate.setNewRoutePath (package:riverpod_navigator/src/routeDelegate.dart:37:22)
    E/flutter ( 5198): #4      RouterDelegate.setInitialRoutePath (package:flutter/src/widgets/router.dart:1171:12)
    E/flutter ( 5198): #5      _RouterState._processParsedRouteInformation.<anonymous closure> (package:flutter/src/widgets/router.dart:636:34)
    E/flutter ( 5198): #6      _RouterState._processParsedRouteInformation.<anonymous closure> (package:flutter/src/widgets/router.dart:632:12)
    E/flutter ( 5198): #7      _rootRunUnary (dart:async/zone.dart:1434:47)
    E/flutter ( 5198): #8      _CustomZone.runUnary (dart:async/zone.dart:1335:19)
    E/flutter ( 5198): <asynchronous suspension>
    

    I don't really know if e.g. I could have made a mistake on my providers but looking at the logs it seems that it points to riverpod_navigator, hence why I thought it might be worth reporting. Otherwise it doesn't crash the application, it just appears on every startup, so it's no really serious.

    My environment

    $ flutter doctor
    [✓] Flutter (Channel stable, 3.0.5, on macOS 12.4 21F79 darwin-arm, locale en-TH)
    [✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    [✓] Xcode - develop for iOS and macOS (Xcode 13.4.1)
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 2020.3)
    [✓] VS Code (version 1.71.0)
    [✓] Connected device (2 available)
    [✓] HTTP Host Availability
    
    • No issues found!
    

    Thanks!

    bug 
    opened by Roms1383 1
  • how to use showDialog()?

    how to use showDialog()?

    Hi, thanks for the library, it really eases my life.

    I have following problem, I need to show popup dialog, so for that I am using showDialog(...) method and when flutter opens it, under the hood it uses: Navigator.of(context, rootNavigator: useRootNavigator).push<T>(DialogRoute<T>(...)

    and for closing uses: Navigator.pop(context) which is messing my context

    for example I have following scenario: screen with form that contains button that calls showDialog, and button to validate the form, when dialog is not shown everything is correct, but when dialog is shown, and after it is closed, I lose all data from the form, my context is clear and when I am clicking validate button nothing happens, until I click to the form widget in any place, then everything is okay again I am pretty new to flutter, so maybe the problem is not in riverpod_navigator at all, pls help me to understand what is the problem and how I can fix it. many thanks

    opened by wizartik 0
  • Deeplink

    Deeplink

    Hello

    My app consists of two main navigation “paths”, one for unauthenticated users (which includes the login screen, registration screen, etc.) and one for authenticated users (which includes content based on a Tabbar-navigation and is therefore a nested navigation).

    Based on that, I have created the following routes:

    AppNavigator: [landingPage, register, login, tabBar] AppNavigator.forPrivate: [home, details] -> Tab_1 AppNavigator.forProfile: [profile, editProfile] -> Tab_2 AppNavigator.forMore: [more, settings] -> Tab_3

    I’m now trying to add Deeplinking into the app. However, any time I attempt to add a route (e.g. ‘profile’ or ‘editProfile’) to the AppNavigator, the Navigator pushes the route onto the AppNavigator stack and not onto the nested stack. For example, trying to parse with the AppNavigator.forMore(navigator.ref).pathParser.fromUrl and setting the route via AppNavigator.forMore(navigator.ref).routerDelegate.setNewRoutePath(segments) pushes the new screen onto he current stack instead of onto the AppNavigator.forMore stack.

    My Deeplink follow the following scheme: deeplink://example_app/tabbar/profile/editProfile

    What I want to archive is this:

    User presses the Deeplink -> App Opens -> appNavigationLogic checks loginStatus. If the user is logged in, navigate to TabBar, select the correct tab and load the page. If not, redirect to login.

    I’ve followed the examples in https://github.com/PavelPZ/riverpod_navigator/blob/main/features/login_flow.md and https://github.com/PavelPZ/riverpod_navigator/blob/main/features/nested_navigation.md and tried to combine them, but unfortunately, I’m now stuck with the aforementioned problems.

    Any advice would be greatly appreciated, thanks!

    enhancement 
    opened by Vermid 15
Releases(v1.0.0)
Owner
pavelpz
pavelpz
A simple and easy to learn declarative navigation framework for Flutter, based on Navigator 2.0.

A simple and easy to learn declarative navigation framework for Flutter, based on Navigator 2.0 (Router). If you love Flutter, you would love declarat

Zeno Nine 20 Jun 28, 2022
Flutter Navigation Best Practices including adapting navigation to platform and branding techniques for navigation surfaces.

Flutter Navigation Best Practices including adapting navigation to platform and branding techniques for navigation surfaces.

Fred Grott 5 Aug 22, 2022
Fast code and awesome design-ui for flutter navigation bar

Flutter-awesome-bottom-navigation-bar ??‍?? Fast code and awesome design-ui for flutter navigation bar ?? Getting Started # First you need to add flas

Hmida 20 Nov 22, 2022
Transparent Android system navigation bar with Flutter and FlexColorScheme package.

Sysnavbar with FlexColorScheme Transparent Android system navigation bar with Flutter and FlexColorScheme. FlexColorScheme V4 Notice If you are using

Rydmike 12 Oct 21, 2022
A Custom Extended Scaffold with Expandable and Floating Navigation Bar

Custom Extended Scaffold Custom Flutter widgets that makes Bottom Navigation Floating and can be expanded with much cleaner a

Ketan Choyal 139 Dec 10, 2022
Flutter Flows made easy! A Flutter package which simplifies navigation flows with a flexible, declarative API.

Flutter Flows made easy! Usage Define a Flow State The flow state will be the state which drives the flow. Each time this state changes, a new navigat

Felix Angelov 337 Dec 31, 2022
Elegant abstraction for complete deep linking navigation in Flutter

Flutter Deep Link Navigation Provides an elegant abstraction for complete deep linking navigation in Flutter. This package only provides deep linking

Dennis Krasnov 64 Dec 27, 2022
A Flutter implementation of a customizable navigation bar with animations.

A heavily customizable bottom navigation bar with some animation modifiers.

null 1 Jun 17, 2022
flutter bottom navigation bat project

bottom_navigation A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you starte

Danushan Ravendran 3 Sep 23, 2021
Custom Bottom navigation bar on Flutter.

Intro Custom Bottom navigation bar on Flutter. The updated one Support : Null safety & Support 9 items on Tabs & Some Color, Size, Effects and font cu

Ihab Zaidi 2 Oct 8, 2021
A flutter navigation package

Create By Me(Agalaba Ifeanyi Precious) go Navigate Navigate Like a pro from one Screen to another Using go navigate. go_Navigate provide you the abili

Agalaba Ifeanyi Precious 2 Oct 11, 2021
Flutter Material Design Navigation Drawer Menu

navigation_drawer_menu Flutter Material Design Navigation Drawer Menu Navigation drawer is a common UI pattern for adaptive menus. The Material Design

Christian Findlay 9 Dec 12, 2022
A Flutter package for easily implementing Material Design navigation transitions.

Morpheus A Flutter package for easily implementing Material Design navigation transitions. Examples Parent-child transition You can use MorpheusPageRo

Sander R. D. Larsen 186 Jan 7, 2023
Customized 🚀 Bottom Navigation Bar Using Flutter 🐦

Customized ?? Bottom Navigation Bar Using Flutter ??

AmirHossein Bayat 3 Dec 7, 2022
Flutter custom BottomBar Navigation Widget

bottom_bar_with_sheet ?? Non-standard way to use more space of screens in your application ?? ?? Custom bottom Sheet under Bottom Navigation Bar ?? ??

Stanislav Ilin 305 Dec 23, 2022
A Flutter package for easy implementation of curved navigation bar

curved_navigation_bar pub package A Flutter package for easy implementation of curved navigation bar. Add dependency dependencies: curved_navigation

null 556 Dec 9, 2022
Persistent Bottom Navigation Bar

Persistent Bottom Navigation Bar A persistent/static bottom navigation bar for Flutter. NOTE: Those migrating from pre 2.0.0 version should check the

Bilal Shahid 421 Dec 20, 2022
A bottom navigation bar that you can customize with the options you need, without any limits.

Bottom Personalized Dot Bar A bottom navigation bar that you can customize with the options you need, without any limits. You can also customize the a

null 103 Oct 20, 2022
Fluro is a Flutter routing library that adds flexible routing options like wildcards, named parameters and clear route definitions.

English | Português The brightest, hippest, coolest router for Flutter. Features Simple route navigation Function handlers (map to a function instead

Luke Pighetti 3.5k Jan 4, 2023