diff --git a/examples/bottom_navigation_riverpod/README.md b/examples/bottom_navigation_riverpod/README.md new file mode 100644 index 0000000..366335d --- /dev/null +++ b/examples/bottom_navigation_riverpod/README.md @@ -0,0 +1,140 @@ +# Bottom Navigation with Riverpod + +Includes multiple meamers, global login guard, and persistent navigation +state using Riverpod. + +

+example-bottom-navigation-riverpod +

+ +## Usage + +Run `flutter create .` to generate all necessary files, if needed. + +## What does this example demonstrate? + +* Two independent nested routers, using two Beamers. When you switch between + two bars back and forth the navigation state in each of them is preserved. + You can also navigate independently in both of them. +* Global login guard. When BeamerGuard detects that the person is not signed + in, then it redirects the user to LoginScreen and won't let you use the app + unless you sing in. The sign in state might be changed at any moment + through authentication provider. +* Navigation state is preserved between application launches. Both Books and + Articles screens remember their last location. The app remembers its + overall last location. Once you restart the app, it restores everything + just where you left it. + +## How example code is structured? + +1. `DATA`: Similarly to other examples, these variables hold fake book data. +1. `SCREENS`: 5 simple widgets that represent parts of the app. +1. `LOCATIONS`: Two main beamer locations for this app. +1. `REPOSITORIES`: Classes to manipulate underlying data sources (the idea + is similar to the **Flutter + Riverpod reference architecture** + [described by Code with Andrea](https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/)). + In this example we store and retrieve data from shared preferences. +1. `CONTROLLERS`: State classes through which Widgets manipulate state and + receive updates when state changes. +1. `PROVIDERS`: Riverpod based providers for repositories and controllers. +1. `APP`: Stateful Widget with a bottom navigation bar. +1. `main()`: Main function that initializes provider scope and creates + MaterialApp. + +## How does it work? + +First of all, bottom navigation bar index in the `AppScreen` stateful widget +is what determines which screen user currently sees: Books or Articles. +The screen themselves are put into an `IndexedStack` within `AppScreen`. + +The value of the bottom navigation bar index changes in response to the +current router location change. For example, when we navigate within the +app or if user puts a new URL into a browser. In order to detect each time +the current routing location changes and react correspondigly, we rely on +`didChangeDependencies()` function overload within our stateful `AppScreen` +widget. It works, because `AppScreen` depends on root `BeamerDelegate` in its +`build()` function. So, whenever Beamer updates and notifies its children, +the `didChangeDependencies()` function gets called. + +Another place where bottom navigation bar index changes is during `onTap()` +invoked when user taps on one of the bottom navigation bar items. We simply +change the state of the `AppScreen` widget through `setState()` and then +call `BeamerDelegate.update(rebuild: false)` function on the router we +are navigating to, either Books or Articles one in our case. + +Root `BeamerDelegate` defines just two routes: `/home/*` and `/login`. This +is done in order to allow global `BeamGuard` check within this root router +delegate to work. It is configured to invoke the check on any route that does +not match the `/login` route. It then reads the authentication state provider +to figure out if the user is signed in. If user is signed out or becomes +signed out, the `BeamGuard` forceflly beams us to the `/login` page. + +In order to allow root `BeamerDelegate` to access riverpod provider values +we define it in the scope of `main()` function, where we also initialize +`ProviderContainer`. This is a rather unorthodox method of using Riverpod. +But it allows us to read provider values outside of BuildContext. This is +exactly what we do by reading the value of authentication state provider when +configuring the `BeamGuard`. + +To remember the last location of the app, we store 3 values within the +navigation state provider: `booksLocation`, `articlesLocation`, and +`lastLocation`. The first one stores the last known location within the Books +screen. The second one does the same for the Articles screen. And finally, +the last one stores the last known location irrespective of which screen +we are on, books or articles. All three of this values need to be updated +every time we navigate. This is done by defining a `routeListener` function +within the root `BeamerDelegate` that gets called each time app location +changes. And every time it does, new value is stored in the navigation state +provider, which through repository ensures that this information is also +written into the shared settings. + +The last piece of the puzzle is to load the last known location, when we +restart the app or sign out and sign in again. The latter is easier to +implement, because we just need to read the last known location from the +provider inside the `onPressed()` function of login button and then beam +to that location. The former, however, proves to be much more problematic. + +The issue here is that we have no way of passing the provider value to the +nested children `BeamerDelegate`s. They have to be defined within the +`AppScreenState` class. We cannot define them globally or within the `main()` +function, because then we have conflicting `GlobalKey`s for Navigator state. +We also cannot define them within the `AppScreen` class itself. They have to +be inside the `AppScreenState` in order for everything, including hot-reload, +to work. Unfortunately, the only way to achieve that is to define a state +constuctor, which is a very unrecommended practive. We pass last known +locations to the state as constructor arguments. This way, when application +reloads it continues off the same place we left it. And even better, the +locations of both of the screens are preserved the way we left them. + +## Demo sequence + +1. Open app → Login screen is shown +1. Try to navigate to /home/books → Stays on Login screen +1. Sign in → Goes to Books screen (default location) + +1. Select second book → Opens "Foundation" book +1. Click back button in top left corner → Goes back to Books screen +1. Select third book → Opens "Fahrenheit 451" book + +1. Choose Articles tab → Goes to Articles screen +1. Select second article → Opens "Flutter Navigator 2.0..." article + +1. Click browser's back button → Goes back to Articles screen +1. Click browser's back button again → Goes back to "Fahrenheit 451" book +1. Click browser's forward button → Goes back to Articles screen +1. Clibk browser's forward button again → Returns to "Flutter Navigator + 2.0..." article + +1. Type first book into browser /home/books/1 → Goes to "Stranger in a + Strange Land" book + +1. Switch between two tabs to show state preservation → Both screen keep + the same state +1. Switch to Articles screen before signing out → Goes to Articles screen + +1. Sign out → Login screen is shown +1. Sign in again → Last open screen (Articles) is shown +1. Switch between two tabs to show state preservation → Tabs keep state + +1. Reload app completely → App starts with the same state +1. Switch between two tabs to show state preservation → Tabs keep state diff --git a/examples/bottom_navigation_riverpod/example-bottom-navigation-riverpod.gif b/examples/bottom_navigation_riverpod/example-bottom-navigation-riverpod.gif new file mode 100644 index 0000000..02a67f2 Binary files /dev/null and b/examples/bottom_navigation_riverpod/example-bottom-navigation-riverpod.gif differ diff --git a/examples/bottom_navigation_riverpod/lib/main.dart b/examples/bottom_navigation_riverpod/lib/main.dart new file mode 100644 index 0000000..08c2aeb --- /dev/null +++ b/examples/bottom_navigation_riverpod/lib/main.dart @@ -0,0 +1,635 @@ +import 'package:flutter/material.dart'; + +import 'package:beamer/beamer.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +// DATA +const List> books = [ + { + 'id': '1', + 'title': 'Stranger in a Strange Land', + 'author': 'Robert A. Heinlein', + }, + { + 'id': '2', + 'title': 'Foundation', + 'author': 'Isaac Asimov', + }, + { + 'id': '3', + 'title': 'Fahrenheit 451', + 'author': 'Ray Bradbury', + }, +]; + +const List> articles = [ + { + 'id': '1', + 'title': 'Explaining Flutter Nav 2.0 and Beamer', + 'author': 'Toby Lewis', + }, + { + 'id': '2', + 'title': 'Flutter Navigator 2.0 for mobile dev: 101', + 'author': 'Lulupointu', + }, + { + 'id': '3', + 'title': 'Flutter: An Easy and Pragmatic Approach to Navigator 2.0', + 'author': 'Marco Muccinelli', + }, +]; + +// SCREENS +class BooksScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + debugLog("BooksScreen | build() | invoked"); + return Scaffold( + appBar: AppBar( + title: Text('Books'), + ), + body: ListView( + children: books + .map( + (book) => ListTile( + title: Text(book['title']!), + subtitle: Text(book['author']!), + onTap: () { + final destination = '/home/books/${book['id']}'; + debugLog("BooksScreen | going to beam to $destination"); + context.beamToNamed(destination); + debugLog("BooksScreen | just beamed to: $destination"); + }, + ), + ) + .toList(), + ), + ); + } +} + +class BookDetailsScreen extends StatelessWidget { + const BookDetailsScreen({required this.book}); + final Map book; + + @override + Widget build(BuildContext context) { + debugLog("BookDetailsScreen | build() | invoked"); + return Scaffold( + appBar: AppBar( + title: Text(book['title']!), + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Text('Author: ${book['author']}'), + ), + ); + } +} + +class ArticlesScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + debugLog("ArticlesScreen | build() | invoked"); + return Scaffold( + appBar: AppBar(title: Text('Articles')), + body: ListView( + children: articles + .map( + (article) => ListTile( + title: Text(article['title']!), + subtitle: Text(article['author']!), + onTap: () { + final destination = '/home/articles/${article['id']}'; + debugLog("ArticlesScreen | going to beam to $destination"); + context.beamToNamed(destination); + debugLog("ArticlesScreen | just beamed to: $destination"); + }, + ), + ) + .toList(), + ), + ); + } +} + +class ArticleDetailsScreen extends StatelessWidget { + const ArticleDetailsScreen({required this.article}); + final Map article; + + @override + Widget build(BuildContext context) { + // debugLog("ArticleDetailsScreen.build invoked"); + return Scaffold( + appBar: AppBar( + title: Text(article['title']!), + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Text('Author: ${article['author']}'), + ), + ); + } +} + +class LoginScreen extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + debugLog("LoginScreen | build() | invoked"); + + final signedIn = ref.read(authStateControllerProvider); + debugLog("LoginScreen | build() | " + "signedIn provider state before returning a Scaffold: $signedIn"); + + return Scaffold( + body: Center( + child: ElevatedButton( + onPressed: () { + final signedInBefore = ref.read(authStateControllerProvider); + debugLog("LoginScreen | ElevatedButton | onPressed() | " + "signedIn state before controller toggle: $signedInBefore"); + ref.read(authStateControllerProvider.notifier).toggleSignIn(); + final signedInAfter = ref.read(authStateControllerProvider); + debugLog("LoginScreen | ElevatedButton | onPressed() | " + "signedIn state after controller toggle: ${signedInAfter}"); + + final lastLocation = + ref.read(navigationStateControllerProvider).lastLocation; + debugLog("LoginScreen | ElevatedButton | onPressed() | " + "going to beam to destination: $lastLocation"); + context.beamToNamed(lastLocation); + debugLog("LoginScreen | ElevatedButton | onPressed() | " + "just beamed to destination: $lastLocation"); + }, + child: signedIn ? const Text('Sign out') : const Text('Sign in'), + ), + ), + ); + } +} + +// LOCATIONS +class BooksLocation extends BeamLocation { + BooksLocation(RouteInformation routeInformation) : super(routeInformation); + + @override + List get pathPatterns => ['/home/books/:bookId']; + + @override + List buildPages(BuildContext context, BeamState state) { + debugLog("BooksLocation | buildPages() | invoked"); + final pages = [ + BeamPage( + key: ValueKey('books'), + title: 'Books', + type: BeamPageType.noTransition, + child: BooksScreen(), + ), + if (state.pathParameters.containsKey('bookId')) + BeamPage( + key: ValueKey('book-${state.pathParameters['bookId']}'), + title: books.firstWhere( + (book) => book['id'] == state.pathParameters['bookId'])['title'], + child: BookDetailsScreen( + book: books.firstWhere( + (book) => book['id'] == state.pathParameters['bookId']), + ), + ), + ]; + + debugLog("BooksLocation | buildPages() | " + "pages to be returned: ${pages.map((page) => page.key)}"); + return pages; + } +} + +class ArticlesLocation extends BeamLocation { + ArticlesLocation(RouteInformation routeInformation) : super(routeInformation); + + @override + List get pathPatterns => ['/home/articles/:articleId']; + + @override + List buildPages(BuildContext context, BeamState state) { + debugLog("ArticlesLocation | buildPages() | invoked"); + final pages = [ + BeamPage( + key: ValueKey('articles'), + title: 'Articles', + type: BeamPageType.noTransition, + child: ArticlesScreen(), + ), + if (state.pathParameters.containsKey('articleId')) + BeamPage( + key: ValueKey('articles-${state.pathParameters['articleId']}'), + title: articles.firstWhere((article) => + article['id'] == state.pathParameters['articleId'])['title'], + child: ArticleDetailsScreen( + article: articles.firstWhere((article) => + article['id'] == state.pathParameters['articleId']), + ), + ), + ]; + + debugLog("ArticlesLocation | buildPages() | " + "pages to be returned: ${pages.map((page) => page.key)}"); + return pages; + } +} + +// REPOSITORIES +class AuthRepository { + const AuthRepository(this.sharedPreferences); + final SharedPreferences sharedPreferences; + + bool get signedIn { + final result = sharedPreferences.getBool('signedIn') ?? false; + debugLog("AuthRepository | get signedIn | " + "signedIn state to be returned: $result"); + return result; + } + + set signedIn(bool state) { + sharedPreferences.setBool('signedIn', state); + debugLog("AuthRepository | set signedIn | " + "new signedIn state was just set: $state"); + } +} + +class NavigationStateRepository { + const NavigationStateRepository(this.sharedPreferences); + final SharedPreferences sharedPreferences; + + String get booksLocation { + final result = + sharedPreferences.getString('booksLocation') ?? '/home/books'; + debugLog("NavigationStateRepository | get booksLocation | " + "location to be returned: $result"); + return result; + } + + set booksLocation(String location) { + sharedPreferences.setString('booksLocation', location); + debugLog("NavigationStateRepository | set booksLocation | " + "new location was just set: $location"); + } + + String get articlesLocation { + final result = + sharedPreferences.getString('articlesLocation') ?? '/home/articles'; + debugLog("NavigationStateRepository | get articlesLocation | " + "location to be returned: $result"); + return result; + } + + set articlesLocation(String location) { + sharedPreferences.setString('articlesLocation', location); + debugLog("NavigationStateRepository | set articlesLocation | " + "new location was just set: $location"); + } + + String get lastLocation { + final result = sharedPreferences.getString('lastLocation') ?? '/home/books'; + debugLog("NavigationStateRepository | get lastLocation | " + "location to be returned: $result"); + return result; + } + + set lastLocation(String location) { + sharedPreferences.setString('lastLocation', location); + debugLog("NavigationStateRepository | set lastLocation | " + "new location was just set: $location"); + } +} + +// CONTROLLERS +class AuthStateController extends Notifier { + @override + bool build() { + final signedIn = ref.watch(authRepositoryProvider).signedIn; + debugLog("AuthStateController | build() | " + "signedIn state to be returned: $signedIn"); + return signedIn; + } + + void toggleSignIn() { + debugLog("AuthStateController | toggleSignIn() | " + "signedIn state before toggle: $state"); + state = !state; + ref.read(authRepositoryProvider).signedIn = state; + debugLog("AuthStateController | toggleSignIn() | " + "signedIn state after toggle: $state"); + } +} + +class NavigationState { + const NavigationState( + this.booksLocation, this.articlesLocation, this.lastLocation); + final String booksLocation; + final String articlesLocation; + final String lastLocation; + + String toString() { + return "NavigationState(" + "booksLocation: $booksLocation, " + "articlesLocation: $articlesLocation, " + "lastLocation: $lastLocation)"; + } +} + +class NavigationStateController extends Notifier { + @override + NavigationState build() { + final provider = ref.watch(navigationStateRepositoryProvider); + final result = NavigationState(provider.booksLocation, + provider.articlesLocation, provider.lastLocation); + debugLog("NavigationStateController | build() | " + "about to return ${result.toString()}"); + return result; + } + + void setBooksLocation(String location) { + debugLog("NavigationStateController | setBooksLocation() | " + "state.booksLocation before: ${state.booksLocation}"); + state = + NavigationState(location, state.articlesLocation, state.lastLocation); + ref.read(navigationStateRepositoryProvider).booksLocation = location; + debugLog("NavigationStateController | setBooksLocation() | " + "state.booksLocation after: ${state.booksLocation}"); + } + + void setArticlesLocation(String location) { + debugLog("NavigationStateController | setArticlesLocation() | " + "state.articlesLocation before: ${state.articlesLocation}"); + state = NavigationState(state.booksLocation, location, state.lastLocation); + ref.read(navigationStateRepositoryProvider).articlesLocation = location; + debugLog("NavigationStateController | setArticlesLocation() | " + "state.articlesLocation after: ${state.articlesLocation}"); + } + + void setLastLocation(String location) { + debugLog("NavigationStateController | setLastLocation() | " + "state.articlesLocation before: ${state.lastLocation}"); + state = + NavigationState(state.booksLocation, state.articlesLocation, location); + ref.read(navigationStateRepositoryProvider).lastLocation = location; + debugLog("NavigationStateController | setLastLocation() | " + "state.articlesLocation after: ${state.lastLocation}"); + } +} + +// PROVIDERS +final sharedPreferencesProvider = Provider((ref) { + throw UnimplementedError(); +}); + +final authRepositoryProvider = Provider((ref) { + final sharedPreferences = ref.watch(sharedPreferencesProvider); + return AuthRepository(sharedPreferences); +}); + +final authStateControllerProvider = + NotifierProvider(AuthStateController.new); + +final navigationStateRepositoryProvider = + Provider((ref) { + final sharedPreferences = ref.watch(sharedPreferencesProvider); + return NavigationStateRepository(sharedPreferences); +}); + +final navigationStateControllerProvider = + NotifierProvider( + NavigationStateController.new); + +// APP +class AppScreen extends ConsumerStatefulWidget { + AppScreen(this.booksLocation, this.articlesLocation, this.lastLocation); + final String booksLocation; + final String articlesLocation; + final String lastLocation; + @override + AppScreenState createState() => + AppScreenState(booksLocation, articlesLocation, lastLocation); +} + +class AppScreenState extends ConsumerState { + AppScreenState( + String booksLocation, String articlesLocation, String lastLocation) + : routerDelegates = [ + BeamerDelegate( + initialPath: booksLocation, + locationBuilder: (routeInformation, _) { + debugLog("AppScreenState | routerDelegates[0] (books) | " + "locationBuilder() | " + "incoming routeInformation: ${routeInformation.location}"); + BeamLocation result = NotFound(path: routeInformation.location!); + if (routeInformation.location!.contains('books')) { + result = BooksLocation(routeInformation); + } + debugLog("AppScreenState | routerDelegates[0] (books) | " + "locationBuilder() | going to return: $result"); + return result; + }, + ), + BeamerDelegate( + initialPath: articlesLocation, + locationBuilder: (routeInformation, _) { + debugLog("AppScreenState | routerDelegates[1] (articles) | " + "locationBuilder() | " + "incoming routeInformation: ${routeInformation.location}"); + BeamLocation result = NotFound(path: routeInformation.location!); + if (routeInformation.location!.contains('articles')) { + result = ArticlesLocation(routeInformation); + } + debugLog("AppScreenState | routerDelegates[1] (articles) | " + "locationBuilder() | going to return: $result"); + return result; + }, + ), + ]; + + late int bottomNavBarIndex; + + final List routerDelegates; + + // This method will be called every time the + // Beamer.of(context) changes. + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final location = Beamer.of(context).configuration.location!; + debugLog("AppScreenState | didChangeDependencies() | " + "uriString read from Beamer.of(context): $location"); + bottomNavBarIndex = location.contains('books') ? 0 : 1; + debugLog("AppScreenState | didChangeDependencies() | " + "computed bottomNavBarIndex: $bottomNavBarIndex"); + } + + @override + Widget build(BuildContext context) { + debugLog("AppScreenState | build() | invoked"); + + return Scaffold( + appBar: AppBar( + title: const Text('Demo App'), + actions: [ + IconButton( + onPressed: () { + final controller = ref.read(authStateControllerProvider.notifier); + controller.toggleSignIn(); + Beamer.of(context).update(); + }, + icon: const Icon(Icons.logout), + ), + ], + ), + body: IndexedStack( + index: bottomNavBarIndex, + children: [ + Beamer(routerDelegate: routerDelegates[0]), + Container( + color: Colors.blueAccent, + padding: const EdgeInsets.all(32.0), + child: Beamer(routerDelegate: routerDelegates[1]), + ), + ], + ), + bottomNavigationBar: BottomNavigationBar( + currentIndex: bottomNavBarIndex, + items: [ + BottomNavigationBarItem(label: 'Books', icon: Icon(Icons.book)), + BottomNavigationBarItem(label: 'Articles', icon: Icon(Icons.article)), + ], + onTap: (index) { + debugLog("AppScreenState | BottomNavigationBar | onTap() | " + "new incoming index value: $index " + "(old value: $bottomNavBarIndex)"); + if (index != bottomNavBarIndex) { + debugLog("AppScreenState | BottomNavigationBar | onTap() | " + "index != bottomNavBarIndex"); + setState(() { + bottomNavBarIndex = index; + }); + routerDelegates[bottomNavBarIndex].update(rebuild: false); + } + }, + ), + ); + } +} + +void main() async { + debugLog("main() | Main function started"); + + WidgetsFlutterBinding.ensureInitialized(); + debugLog("main() | WidgetsFlutterBinding.ensureInitialized executed"); + + Beamer.setPathUrlStrategy(); + debugLog("main() | Beamer.setPathUrlStrategy executed"); + + final sharedPreferences = await SharedPreferences.getInstance(); + debugLog("main() | sharedPreferences instance obtained"); + + final container = ProviderContainer( + overrides: [ + sharedPreferencesProvider.overrideWithValue(sharedPreferences), + ], + ); + + final booksInitialPath = + container.read(navigationStateControllerProvider).booksLocation; + final articlesInitialPath = + container.read(navigationStateControllerProvider).articlesLocation; + final lastInitialPath = + container.read(navigationStateControllerProvider).lastLocation; + + final routerDelegate = BeamerDelegate( + initialPath: lastInitialPath, + locationBuilder: RoutesLocationBuilder( + routes: { + '/home/*': (context, state, data) => + AppScreen(booksInitialPath, articlesInitialPath, lastInitialPath), + '/login': (context, state, data) => BeamPage( + key: ValueKey('login'), + title: 'Login', + child: LoginScreen(), + ), + }, + ), + buildListener: (context, delegate) { + final location = Beamer.of(context).configuration.location; + debugLog("routerDelegate | buildListener() | " + "location: $location"); + }, + routeListener: (routeInformation, delegate) { + final location = routeInformation.location; + + Future(() { + if (location != null) { + debugLog("routerDelegate | routeListener() | " + "about to save location: $location"); + + if (location.startsWith('/home/books') || + location.startsWith('/home/articles')) { + container + .read(navigationStateControllerProvider.notifier) + .setLastLocation(location); + debugLog("routerDelegate | routeListener() | " + "just saved last location: $location"); + } + + if (location.startsWith('/home/books')) { + container + .read(navigationStateControllerProvider.notifier) + .setBooksLocation(location); + debugLog("routerDelegate | routeListener() | " + "just saved books location: $location"); + } else if (location.startsWith('/home/articles')) { + container + .read(navigationStateControllerProvider.notifier) + .setArticlesLocation(location); + debugLog("routerDelegate | routeListener() | " + "just saved articles location: $location"); + } + } + }); + }, + guards: [ + BeamGuard( + pathPatterns: ['/login'], + guardNonMatching: true, + check: (context, state) { + debugLog("routerDelegate | " + "BeamGuard | check() | is about to retrieve signedIn state"); + final signedIn = container.read(authStateControllerProvider); + debugLog("routerDelegate | " + "BeamGuard | check() | obtained signedIn state: $signedIn"); + return signedIn; + }, + beamToNamed: (origin, target, deepLink) => '/login', + ), + ], + ); + + runApp( + UncontrolledProviderScope( + container: container, + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + routerDelegate: routerDelegate, + routeInformationParser: BeamerParser(), + backButtonDispatcher: BeamerBackButtonDispatcher( + delegate: routerDelegate, + ), + ), + ), + ); +} + +void debugLog(String value) { + final now = DateTime.now(); + print("[$now] $value"); +} diff --git a/examples/bottom_navigation_riverpod/pubspec.yaml b/examples/bottom_navigation_riverpod/pubspec.yaml new file mode 100644 index 0000000..c9497ae --- /dev/null +++ b/examples/bottom_navigation_riverpod/pubspec.yaml @@ -0,0 +1,26 @@ +name: bottom_navigation_riverpod +description: Bottom navigation with multiple beamers, login guard, and navigation state managed by riverpod. + +publish_to: "none" + +version: 1.0.0+1 + +environment: + sdk: ">=2.17.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + beamer: + path: ../../package + + cupertino_icons: ^1.0.3 + flutter_riverpod: ^2.3.1 + shared_preferences: ^2.0.18 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true