diff --git a/example/lib/main.dart b/example/lib/main.dart index de27b93..04e7b44 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'router/router_listenable.dart'; import 'router/routes.dart'; -import 'state/auth_state.dart'; import 'utils/state_logger.dart'; void main() { @@ -41,100 +40,8 @@ class MyAwesomeApp extends HookConsumerWidget { routerConfig: router, title: 'hooks_riverpod + go_router Demo', theme: ThemeData( - primarySwatch: Colors.blue, + primarySwatch: Colors.cyan, ), ); } } - -class HomePage extends ConsumerWidget { - const HomePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar(title: const Text('Your phenomenal app')), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Home Page'), - ElevatedButton( - onPressed: ref.read(authNotifierProvider.notifier).logout, - child: const Text('Logout'), - ), - ], - ), - ), - ); - } -} - -class LoginPage extends ConsumerWidget { - const LoginPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Login Page'), - ElevatedButton( - onPressed: () => ref.read(authNotifierProvider.notifier).login( - 'myEmail', - 'myPassword', - ), - child: const Text('Login'), - ), - ], - ), - ), - ); - } -} - -class SplashPage extends StatelessWidget { - const SplashPage({super.key}); - - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Center(child: Text('Splash Page')), - ); - } -} - -class AdminPage extends StatelessWidget { - const AdminPage({super.key}); - - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Center(child: Text('Admin Page')), - ); - } -} - -class UserPage extends StatelessWidget { - const UserPage({super.key}); - - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Center(child: Text('User Page')), - ); - } -} - -class GuestPage extends StatelessWidget { - const GuestPage({super.key}); - - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Center(child: Text('Guest Page')), - ); - } -} diff --git a/example/lib/pages/admin_page.dart b/example/lib/pages/admin_page.dart new file mode 100644 index 0000000..7c8a804 --- /dev/null +++ b/example/lib/pages/admin_page.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../state/auth_controller.dart'; +import '../state/cute_rabbits.dart'; +import '../state/nuclear_codes.dart'; +import '../widgets/my_sliver_list.dart'; +import '../widgets/user_title.dart'; + +class AdminPage extends ConsumerWidget { + const AdminPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final codes = ref.watch(nuclearCodesProvider); + final rabbits = ref.watch(cuteRabbitsProvider); + + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 24), + child: Text('Admin Page', style: TextStyle(fontWeight: FontWeight.bold)), + ), + const SizedBox(height: 20), + const Text( + 'Woah, just look at the stuff you could do in here.', + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + Flexible( + child: CustomScrollView( + slivers: [ + const SliverToBoxAdapter(child: _AdminTitle()), + MySliverList(elements: codes, isNuke: true), + const SliverToBoxAdapter(child: UserTitle()), + MySliverList(elements: rabbits), + ], + ), + ), + const SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: ElevatedButton.icon( + onPressed: ref.read(authControllerProvider.notifier).logout, + icon: const Icon(Icons.logout), + label: const Text('Logout'), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _AdminTitle extends StatelessWidget { + const _AdminTitle(); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(vertical: 32), + child: Text( + '☢️ Admin? Here, use these nukes ☢️', + style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + ); + } +} diff --git a/example/lib/pages/details_page.dart b/example/lib/pages/details_page.dart new file mode 100644 index 0000000..43d3d4a --- /dev/null +++ b/example/lib/pages/details_page.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class DetailsPage extends ConsumerWidget { + const DetailsPage(this.detailCode, {required this.isNuclearCode, super.key}); + final int detailCode; + final bool isNuclearCode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final title = isNuclearCode + ? "Here's your nuke. Use them carefully, like, don't drop 'em, k?" + : "Here's your unique identifier of your new fluffy friend 😍"; + + return Scaffold( + body: SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + const SizedBox(height: 60), + Text('$detailCode'), + const SizedBox(height: 60), + ElevatedButton.icon( + onPressed: () => context.pop(true), + icon: const Icon(Icons.explicit), + label: const Text('Make it go boom.'), + ), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: () => context.pop(false), + icon: const Icon(Icons.home), + label: const Text('Take it home and take care of it.'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/pages/guest_page.dart b/example/lib/pages/guest_page.dart new file mode 100644 index 0000000..d3dd2fb --- /dev/null +++ b/example/lib/pages/guest_page.dart @@ -0,0 +1,31 @@ +import 'package:example/state/auth_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class GuestPage extends ConsumerWidget { + const GuestPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Guest Page', style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 20), + const Text("It's not like you can do much, here."), + const SizedBox(height: 8), + const Text("After all, you're a guest"), + const SizedBox(height: 40), + ElevatedButton.icon( + onPressed: ref.read(authControllerProvider.notifier).logout, + icon: const Icon(Icons.logout), + label: const Text('Logout'), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart new file mode 100644 index 0000000..0a92ae0 --- /dev/null +++ b/example/lib/pages/home_page.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../state/auth_controller.dart'; + +class HomePage extends ConsumerWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('Your phenomenal app')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Home Page'), + const Text("There's nothing much you can do, here"), + ElevatedButton( + onPressed: ref.read(authControllerProvider.notifier).logout, + child: const Text('Logout'), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/login_page.dart b/example/lib/pages/login_page.dart new file mode 100644 index 0000000..752c080 --- /dev/null +++ b/example/lib/pages/login_page.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../state/auth_controller.dart'; + +class LoginPage extends ConsumerWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Login Page'), + ElevatedButton( + onPressed: () => ref.read(authControllerProvider.notifier).login( + 'myEmail', + 'myPassword', + ), + child: const Text('Login'), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/splash_page.dart b/example/lib/pages/splash_page.dart new file mode 100644 index 0000000..16a8acc --- /dev/null +++ b/example/lib/pages/splash_page.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class SplashPage extends StatelessWidget { + const SplashPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center(child: Text('Splash Page')), + ); + } +} diff --git a/example/lib/pages/user_page.dart b/example/lib/pages/user_page.dart new file mode 100644 index 0000000..7a623f1 --- /dev/null +++ b/example/lib/pages/user_page.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../state/auth_controller.dart'; +import '../state/cute_rabbits.dart'; +import '../widgets/my_sliver_list.dart'; +import '../widgets/user_title.dart'; + +class UserPage extends ConsumerWidget { + const UserPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final rabbits = ref.watch(cuteRabbitsProvider); + + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 24), + child: Text('User Page', style: TextStyle(fontWeight: FontWeight.bold)), + ), + const SizedBox(height: 20), + const Text( + "Looks like you've got enough permissions to... pick up some rabbits 😍", + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + Flexible( + child: CustomScrollView( + slivers: [ + const SliverToBoxAdapter(child: UserTitle()), + MySliverList(elements: rabbits), + ], + ), + ), + const SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: ElevatedButton.icon( + onPressed: ref.read(authControllerProvider.notifier).logout, + icon: const Icon(Icons.logout), + label: const Text('Logout'), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/example/lib/router/router_listenable.dart b/example/lib/router/router_listenable.dart index 28e0a76..e9244dd 100644 --- a/example/lib/router/router_listenable.dart +++ b/example/lib/router/router_listenable.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../state/auth_state.dart'; +import '../state/auth_controller.dart'; import 'routes.dart'; part 'router_listenable.g.dart'; @@ -33,7 +33,7 @@ class RouterListenable extends _$RouterListenable implements Listenable { // One could watch more providers and write logic accordingly _isAuth = await ref.watch( - authNotifierProvider.selectAsync( + authControllerProvider.selectAsync( (data) => data.map(signedIn: (_) => true, signedOut: (_) => false), ), ); diff --git a/example/lib/router/routes.dart b/example/lib/router/routes.dart index 72dc3dd..9285c91 100644 --- a/example/lib/router/routes.dart +++ b/example/lib/router/routes.dart @@ -1,10 +1,16 @@ import 'dart:async'; +import 'package:example/pages/details_page.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../main.dart'; +import '../pages/admin_page.dart'; +import '../pages/guest_page.dart'; +import '../pages/home_page.dart'; +import '../pages/login_page.dart'; +import '../pages/splash_page.dart'; +import '../pages/user_page.dart'; import '../state/permissions.dart'; part 'routes.g.dart'; @@ -96,3 +102,19 @@ class GuestRoute extends GoRouteData { return const GuestPage(); } } + +@TypedGoRoute(path: DetailsRoute.path) +class DetailsRoute extends GoRouteData { + const DetailsRoute(this.id, {this.isNuke = false}); + final int id; + final bool isNuke; + static const path = '/details/:id'; + + @override + Widget build(BuildContext context, GoRouterState state) { + return DetailsPage( + id, + isNuclearCode: isNuke, + ); + } +} diff --git a/example/lib/state/auth_state.dart b/example/lib/state/auth_controller.dart similarity index 95% rename from example/lib/state/auth_state.dart rename to example/lib/state/auth_controller.dart index db8d8e8..448f59a 100644 --- a/example/lib/state/auth_state.dart +++ b/example/lib/state/auth_controller.dart @@ -6,7 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../entities/auth.dart'; -part 'auth_state.g.dart'; +part 'auth_controller.g.dart'; /// A mock of an Authenticated User const _dummyUser = Auth.signedIn( @@ -16,9 +16,9 @@ const _dummyUser = Auth.signedIn( token: 'some-updated-secret-auth-token', ); -/// This notifier holds and handles the authentication state of the application +/// This controller is an [AsyncNotifier] that holds and handles our authentication state @riverpod -class AuthNotifier extends _$AuthNotifier { +class AuthController extends _$AuthController { late SharedPreferences _sharedPreferences; static const _sharedPrefsKey = 'token'; diff --git a/example/lib/state/cute_rabbits.dart b/example/lib/state/cute_rabbits.dart new file mode 100644 index 0000000..90c9749 --- /dev/null +++ b/example/lib/state/cute_rabbits.dart @@ -0,0 +1,24 @@ +import 'package:example/utils/cache_for.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'cute_rabbits.g.dart'; + +@riverpod +FutureOr> cuteRabbits(CuteRabbitsRef ref) async { + // A proper mock of a simple request. I guess adopting a rabbit is simple. + await Future.delayed(800.milliseconds); + + final result = [ + (Icons.cruelty_free, 'A fluffy, cute, rabbit.'), + (Icons.cruelty_free_outlined, 'Wow, this looks even fluffier!'), + (Icons.cruelty_free_sharp, "Why must I choose, I want 'em all!"), + (Icons.cruelty_free_rounded, 'This is all puffy 😍'), + (Icons.pets, 'Uhm... we also could see more pets...'), + (Icons.flutter_dash, 'Oh. My. God.\nI. Want. This.'), + ]; + + ref.cacheFor(2.minutes); + return result; +} diff --git a/example/lib/state/nuclear_codes.dart b/example/lib/state/nuclear_codes.dart new file mode 100644 index 0000000..825bb27 --- /dev/null +++ b/example/lib/state/nuclear_codes.dart @@ -0,0 +1,28 @@ +import 'package:example/utils/cache_for.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'nuclear_codes.g.dart'; + +@riverpod +FutureOr> nuclearCodes(NuclearCodesRef ref) async { + // A proper mock of a simple request. I guess requesting nuclear codes is simple. + await Future.delayed(800.milliseconds); + + final result = [ + (Icons.one_k, 'Wow, this looks totally safe; click and see the nuclear code!'), + (Icons.two_k, 'Wow, this looks totally safe; click and see the nuclear code!'), + (Icons.three_k, 'Wow, this looks totally safe; click and see the nuclear code!'), + (Icons.four_k, 'Uhm... do we *actually* want to see these codes?'), + (Icons.five_k, 'Uhm... do we *actually* want to see these codes?'), + (Icons.six_k, 'Uhm... do we *actually* want to see these codes?'), + (Icons.seven_k, 'Uhm... do we *actually* want to see these codes?'), + (Icons.eight_k, 'Ok, this is WAY too hot to handle.'), + (Icons.nine_k, 'Ok, this is WAY too hot to handle.'), + (Icons.ten_k, 'Ok, this is WAY too hot to handle.'), + ]; + + ref.cacheFor(10.seconds); // Nukes are dangerous - I guess - so let's cache less time + return result; +} diff --git a/example/lib/state/permissions.dart b/example/lib/state/permissions.dart index 4616943..893918b 100644 --- a/example/lib/state/permissions.dart +++ b/example/lib/state/permissions.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../entities/user_role.dart'; -import 'auth_state.dart'; +import 'auth_controller.dart'; part 'permissions.g.dart'; @@ -12,7 +12,7 @@ part 'permissions.g.dart'; @riverpod Future permissions(PermissionsRef ref) async { final userId = await ref.watch( - authNotifierProvider.selectAsync( + authControllerProvider.selectAsync( (value) => value.map( signedIn: (signedIn) => signedIn.id, signedOut: (signedOut) => null, diff --git a/example/lib/utils/cache_for.dart b/example/lib/utils/cache_for.dart new file mode 100644 index 0000000..30367bb --- /dev/null +++ b/example/lib/utils/cache_for.dart @@ -0,0 +1,12 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +extension CacheFor on AutoDisposeRef { + void cacheFor(Duration duration) { + final link = keepAlive(); + final timer = Timer(duration, link.close); + + onDispose(timer.cancel); + } +} diff --git a/example/lib/widgets/my_sliver_list.dart b/example/lib/widgets/my_sliver_list.dart new file mode 100644 index 0000000..541bb3d --- /dev/null +++ b/example/lib/widgets/my_sliver_list.dart @@ -0,0 +1,66 @@ +import 'dart:math' as math; + +import 'package:example/router/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class MySliverList extends StatelessWidget { + const MySliverList({ + super.key, + required this.elements, + this.isNuke = false, + }); + final AsyncValue> elements; + final bool isNuke; + + @override + Widget build(BuildContext context) { + return SliverList.list( + children: elements.when( + data: (data) { + return [ + for (final (icon, title) in data) + Container( + margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), + child: ListTile( + onTap: () async { + final id = icon.hashCode + title.hashCode; + final willItExplode = + await DetailsRoute(id, isNuke: isNuke).push(context); + + if (willItExplode == null || !willItExplode) return; + if (!context.mounted) return; + + final object = isNuke ? 'nuke' : 'cute rabbit'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Sorry, but your $object exploded. Best luck next time!'), + ), + ); + }, + tileColor: Color(_randomColorValue).withOpacity(0.05), + leading: Icon( + icon, + color: Color(_randomColorValue).withOpacity(0.8), + size: 32, + ), + title: Text(title), + ), + ) + ]; + }, + error: (error, stackTrace) => [ + const Text('Woah, something went wrong'), + const SizedBox(height: 20), + const Text('Welp, home nothing goes boom, yet') + ], + loading: () => [ + const Center(child: CircularProgressIndicator()), + const Text('Loading your nuclear codes...') + ], + ), + ); + } + + int get _randomColorValue => (math.Random().nextDouble() * 0xFFFFFF).toInt(); +} diff --git a/example/lib/widgets/user_title.dart b/example/lib/widgets/user_title.dart new file mode 100644 index 0000000..a8e7860 --- /dev/null +++ b/example/lib/widgets/user_title.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class UserTitle extends StatelessWidget { + const UserTitle({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(vertical: 32), + child: Text( + '🐰 Hello, User! Select your rabbits 🐰', + style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + ); + } +} diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index 78be1e6..1cfee0a 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -1,5 +1,5 @@ import 'package:example/main.dart'; -import 'package:example/state/auth_state.dart'; +import 'package:example/state/auth_controller.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart';