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

outdated

Simple but powerfull Flutter navigation with riverpod and Navigator 2.0

Riverpod navigation #

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

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<TypedSegment>
  • 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();

  /// used for creating HomeSegment from URL pars
  factory HomeSegment.fromUrlPars(UrlPars pars) => const HomeSegment();
}

class BookSegment extends TypedSegment {
  const BookSegment({required this.id});

  /// used for creating BookSegment from URL pars
  factory BookSegment.fromUrlPars(UrlPars pars) => BookSegment(id: pars.getInt('id'));

  /// used for encoding BookSegment props to URL pars
  @override
  void toUrlPars(UrlPars pars) => pars.setInt('id', id);

  final int id;
}

Note: fromUrlPars and toUrlPars 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'
            /// fromUrlPars is used to decode URL to segment
            /// HomeScreen.new and BookScreen.new are screen builders for a given segment
            RRoute<HomeSegment>(
              'home',
              HomeSegment.fromUrlPars,
              HomeScreen.new,
            ),
            RRoute<BookSegment>(
              'book',
              BookSegment.fromUrlPars,
              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(
        // home path 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.

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.

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: 
...

And that's all

See:

Note: The link Go to book: [3, 13, 103] in the running example would not make much sense in the real Books application. 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

<unique TypedSegment id>[;<property name>=<property value>]*

e.g. book;id=3.

Instead of directly converting to/from the string, we convert to/from typedef UrlPars = Map<String,String>, e.g.:

  factory BookSegment.fromUrlPars(UrlPars pars) => BookSegment(id: pars.getInt('id'));
  @override
  void toUrlPars(UrlPars pars) => pars.setInt('id', id);

So far, we support the following types of TypedSegment property: int, double, bool, String, int?, double?, bool?, String?. See extension UrlParsEx on UrlPars in path_parser.dart.

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

  • support another property type (as a DateTime, providing getDateTime, getDateTimeNull and setDateTime UrlPars's extension)
  • 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),
...         

TestSegment example:

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

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

  @override
  void toUrlPars(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;
}

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 code, 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');

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.

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
25
likes
0
pub points
57%
popularity

Publisher

unverified uploader

Simple but powerfull Flutter navigation with riverpod and Navigator 2.0

Repository (GitHub)
View/report issues

License

unknown (LICENSE)

Dependencies

flutter, hooks_riverpod, meta, riverpod_navigator_core

More

Packages that depend on riverpod_navigator