riverpod_navigator 0.14.0 copy "riverpod_navigator: ^0.14.0" to clipboard
riverpod_navigator: ^0.14.0 copied to clipboard

outdated

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 use navigate([Home(), Books(), Book(id: bookId)]); instead of navigate('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 related to url path home/books/book;id=2

  • string-path: final stringPath = 'home/books/book;id=2';
  • string-segment - the string-path consists of three string-segments: 'home', 'books' and 'book;id=2'
  • typed-segment - the typed-segment is immutable class that defines string-segment: HomeSegment(), BooksSegment() and BookSegment(id:2)
  • typed-path: typed-path can be understood as List
  • navigation-stack of Flutter Navigator 2.0 is a stack of screens, parameterized by typed-segment: 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.

Note: There is a one-to-one relationship between the given segment and the screen (HomeSegment - HomeScreen, BookSegment - BookScreen). In the following text, I sometimes confuse the two terms..

Installation #

After clonning repository, go to examples\doc subdirectory and execute:

  • flutter create .
  • flutter pub get
  • flutter pub run build_runner build --delete-conflicting-outputs

Simple example #

Step1 - imutable classes for typed-segment #

We use freezed-package for generation immutable clasess (that defines typed-segment's).

It's a good idea to be familiar with the freezed-package (including support for JSON serialization).

From the following SegmentGrp class declaration, the freezed package generates two classes: HomeSegment and PageSegment.

@freezed
class SegmentGrp with _$SegmentGrp, TypedSegment {
  SegmentGrp._();
  factory SegmentGrp.home() = HomeSegment;
  factory SegmentGrp.page({required String title}) = PageSegment;

  factory SegmentGrp.fromJson(Map<String, dynamic> json) => _$SimpleSegmentFromJson(json);
}

Step2 - navigator parameterization #

Extends the RiverpodNavigator class as follows:

class AppNavigator extends RiverpodNavigator {
  AppNavigator(Ref ref)
      : super(
          ref,
          // which screen to run when the application starts
          [HomeSegment()],
          [
            // JSON serialization of HomeSegment and PageSegment
            RRoutes<SegmentGrp>(SegmentGrp.fromJson, [
              // build a screen from segment
              RRoute<HomeSegment>(HomeScreen.new),
              RRoute<PageSegment>(PageScreen.new),
            ])
          ],
        );
}

Step3 - use the RiverpodNavigator 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) {
    // for all widgets with riverpod support, the navigator is available via riverpodNavigatorProvider
    final navigator = ref.read(riverpodNavigatorProvider);
    return MaterialApp.router(
      title: 'Riverpod Navigator Example',
      routerDelegate: navigator.routerDelegate,
      routeInformationParser: navigator.routeInformationParser,
    );
  }
}

Step4 - runApp #

void main() => runApp(
      ProviderScope(
        overrides: [
          riverpodNavigatorCreatorProvider.overrideWithValue(AppNavigator.new),
        ],
        child: const App(),
      ),
    );

Step5 - widgets for screens #

Creating screen widgets is probably an understandable part of the example.

Only the navigation to the new screen is interesting:

//  getting navigation stack "HomeScreen(HomeSegment()) => PageScreen(PageSegment(title: 'Page title'))".
ref.read(riverpodNavigatorProvider).navigate([HomeSegment(), PageSegment(title: 'Page')]);

or

// getting navigation stack "HomeScreen(HomeSegment())".
ref.read(riverpodNavigatorProvider).navigate([HomeSegment()]);

Code of the example

The full code is available here: simple.dart.

Other feartures #

Code simplification #

Subsequent examples are prepared with simpler code:

  • using the functional_widget package simplifies widgets typing
  • some code is moved to common dart file

A modified version of the first example is here: simple_modified.dart.

Async navigation and splash screen #

Navigation is delayed until the asynchronous actions are performed. These actions are:

  • creating (before inserting a new screen into the navigation stack)
  • deactivating (before removing the old screen from the navigation stack)
  • merging (before screen replacement with the same segment type in the navigation stack)
// simulates an async action such as loading external data or saving to external storage
Future<String> simulateAsyncResult(String actionName, int msec) async {
  await Future.delayed(Duration(milliseconds: msec));
  return '$actionName: async result after $msec msec';
}

class AppNavigator extends RiverpodNavigator {
  AppNavigator(Ref ref)
      : super(
          ref,
          [HomeSegment()],
          [
            RRoutes<SegmentGrp>(SegmentGrp.fromJson, [
              RRoute<HomeSegment>(
                HomeScreen.new,
                creating: (newSegment) => simulateAsyncResult('Home.creating', 2000),
              ),
              RRoute<PageSegment>(
                PageScreen.new,
                creating: (newSegment) => simulateAsyncResult('Page.creating', 400),
                merging: (oldSegment, newSegment) => simulateAsyncResult('Page.merging', 200),
                deactivating: null,
              ),
            ])
          ],
        );
}

Code of the example

See async.dart

Login flow application #

A slightly more complicated example, implementing a login flow as follows:

  1. there is a home screen, five book screens (with id = 1...5) and a login screen
  2. each screen (except login one) has a Login x Logout button
  3. the book screen with odd 'id' is not accessible without login (for such screens the application is redirected to the login page)
  4. after logging in, the application redirects to the page that requires a login

Code of the example

See login_flow.dart.

Testing #

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

Code of the example

See login_flow_test.dart.

More TypedSegment roots #

In a real application with many dozens of screens, it would not be practical to define typed-segments using one class (as SegmentGrp is). Use the unique "unionKey" for the second and next segment group.

!!!! jsonNameSpace for @Freezed(unionKey: SecondGrp.jsonNameSpace) must start with an underscore. !!!!

!!!! There must be at least two factory constructors in one class !!!!

Code of the example

See more_groups.dart.

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.

What's under the hood #

A brief introduction to the riverpod_navigation principle can help with its use.

How is our mission "to keep string-path <= typed-path => navigation-stack always in sync" implemented?

Let's look at the principles of how to implement login app flow.

In the beginning there are riverpod providers and their states #

/// All "typed-segments" (eg HomeSegment and PageSegment from example) are inherited from this class
abstract class TypedSegment {}
/// **typed-path**
typedef TypedPath = List<TypedSegment>;

/// this TypedPath provider is part of the riverpod_navigation package
final ongoingPathProvider = StateProvider<TypedPath>((_) => []);

/// another provider with a "userIsLogged state" on which the navigation state depends 
/// (and which can be part of the application)
final userIsLoggedProvider = StateProvider<bool>((_) => false);

...
//and of course HomeSegment, HomeScreen, LoginSegment, LoginScreen, PageSegment and PageScreen
...

At the end is the navigation stack, represented by Flutter Navigator 2.0 RouterDelegate #

class RiverpodRouterDelegate extends RouterDelegate<TypedPath> {
  ...
  /// current navigation state
  @override
  TypedPath currentConfiguration = [];
  ...
  /// build screens from currentConfiguration 
  @override
  Widget build(BuildContext context) => Navigator(
      pages: currentConfiguration.map((typedSegment) => <... create screen for given typedSegment ...>,
      ...
  )
  /// a notifyListeners notifies RouterDelegate that it needs to be rebuilt
  @override
  void notifyListeners() : super.notifyListeners();
}

And in the middle is RiverpodNavigator #

RiverpodNavigator reacts to changes of the input states (ongoingPathProvider, userIsLoggedProvider in this case) and updates the output state (navigation stack) accordingly.

How is it done?

class RiverpodNavigator {
  RiverpodNavigator(Ref ref) {
    ...
    /// Listen to the providers and call "onStateChanged" every time they change.
    [ongoingPathProvider,userIsLoggedProvider].foreach((provider) => ref.listen(provider, (_,__) => onStateChanged())));
  }

  void onStateChanged() {
    //=====> at this point, "ongoingPathProvider state" and "riverpodRouterDelegate.currentConfiguration" could differ
    // get ongoingPath notifier
    final ongoingPathNotifier = ref.read(ongoingPathProvider.notifier);
    // run app specific application navigation logic here (redirection, login, etc.).
    final newOngoingPath = appNavigationLogic(ongoingPathNotifier.state);
    // actualize a possibly changed ongoingPath
    ongoingPathNotifier.state = newOngoingPath;
    // the next two lines will cause Flutter Navigator 2.0 to update the navigation stack according to the ongoingPathProvider state
    riverpodRouterDelegate.currentConfiguration = newOngoingPath;
    riverpodRouterDelegate.notifyListeners();
    //=====> at this point, "ongoingPathProvider state" and  "RiverpodRouterDelegate" are in sync
  }

  /// Enter application navigation logic here (redirection, login, etc.). 
  /// No need to override (eg when the navigation status depends only on the ongoingPathProvider and no redirects or no route guard is required)
  TypedPath appNavigationLogic(TypedPath ongoingPath) => ongoingPath;
}

Example of appNavigationLogic for Login flow #

@override 
TypedPath appNavigationLogic(TypedPath ongoingPath) {
  final userIsLogged = ref.read(userIsLoggedProvider);

  // if user is not logged in and some of screen in navigations stack needs login => redirect to LoginScreen
  if (!userIsLogged && ongoingPath.any((segment) => needsLogin(segment)) return [LoginSegment()];

  // user is logged and LogginScreen is going to display => redirect to HomeScreen
  if (userIsLogged && ongoingPath.any((segment) => segment is LoginSegment) return [HomeSegment()];)

  // no redirection is needed
  return ongoingPath;
}

Note: we need the "needsLogin" function that returns true when a login is required for given screen

Thats it #

This is all essential for the implementation of the login flow. With Riverpod, using Flutter Navigator 2.0 is really easy. See how the Loggin button looks:

Login Button

Consumer(builder: (_, ref, __) {
  final userIsLoggedNotifier = ref.watch(userIsLoggedProvider.notifier);
  return ElevatedButton(
    // toogles the login state
    onPressed: () => userIsLoggedNotifier.update((s) => !s),
    // displays correct login button text
    child: Text(userIsLoggedNotifier.state ? 'Logout' : 'Login'),
  );
}),

Verification of the riverpod_navigator idea #

If anyone wants to understand in detail 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.

Roadmap #

I prepared this package for my new project. Its further development depends on whether it will be used by the community.

  • finish examples
  • 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
25
likes
0
pub points
60%
popularity

Publisher

unverified uploader

Simple but powerfull Flutter navigation with riverpod, freezed and Navigator 2.0.

Repository (GitHub)
View/report issues

License

unknown (LICENSE)

Dependencies

flutter, freezed_annotation, hooks_riverpod, json_annotation, meta, riverpod, tuple

More

Packages that depend on riverpod_navigator