Last active
February 24, 2023 21:34
-
-
Save TekExplorer/5743e1397d2beadaedddfebe9826781a to your computer and use it in GitHub Desktop.
My best riverpod mutation setup yet!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:math'; | |
| import 'package:flutter/material.dart'; | |
| // could use flutter_riverpod | |
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | |
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | |
| import 'mutation_state.dart'; | |
| part 'main.g.dart'; | |
| void main(List<String> args) { | |
| runApp(const ProviderScope( | |
| child: MaterialApp( | |
| home: Scaffold( | |
| body: TodosWidget(), | |
| ), | |
| ), | |
| )); | |
| } | |
| class Todo { | |
| Todo({required this.title}); | |
| final String timeCreated = DateTime.now().toString(); | |
| final String title; | |
| } | |
| @riverpod | |
| class TodosNotifier extends _$TodosNotifier { | |
| @override | |
| Future<List<Todo>> build() async { | |
| final list = <Todo>[]; | |
| for (var i = 0; i < 10; i++) { | |
| await Future<void>.delayed(const Duration(seconds: 1)); | |
| list.add(Todo(title: 'Todo $i')); | |
| } | |
| return list; | |
| } | |
| /// example notifier function. randomly throws for demonstration purposes | |
| Future<void> addTodo(Todo todo) async { | |
| await Future<void>.delayed(const Duration(seconds: 1), () { | |
| if (Random().nextBool()) { | |
| throw Exception('Failed to add todo'); | |
| } | |
| }); | |
| state = AsyncData([...await future, todo]); | |
| } | |
| } | |
| @riverpod | |
| MutationState<void, Todo> addTodo(AddTodoRef ref) { | |
| // user passes in a `Todo` | |
| // addTodo is called, with error handling by this provider | |
| // could use a tearoff | |
| return MutationState.create( | |
| ref, | |
| (todo) async => | |
| await ref.read(todosNotifierProvider.notifier).addTodo(todo)); | |
| } | |
| class TodosWidget extends ConsumerWidget { | |
| const TodosWidget({super.key}); | |
| @override | |
| Widget build(BuildContext context, WidgetRef ref) { | |
| final todos = ref.watch(todosNotifierProvider); | |
| return todos.when( | |
| loading: () => const Center(child: CircularProgressIndicator()), | |
| error: (e, s) => Center(child: Text('Error: $e')), | |
| data: (data) { | |
| return Column( | |
| children: [ | |
| Expanded( | |
| child: ListView.builder( | |
| itemCount: data.length, | |
| itemBuilder: (context, index) { | |
| return TodoTile(todo: data[index]); | |
| }, | |
| ), | |
| ), | |
| const AddTodoTile(), | |
| ], | |
| ); | |
| }, | |
| ); | |
| } | |
| } | |
| class TodoTile extends StatelessWidget { | |
| const TodoTile({super.key, required this.todo}); | |
| final Todo todo; | |
| @override | |
| Widget build(BuildContext context) { | |
| return ListTile( | |
| title: Text(todo.title), | |
| subtitle: Text(todo.timeCreated), | |
| ); | |
| } | |
| } | |
| class AddTodoTile extends ConsumerWidget { | |
| const AddTodoTile({super.key}); | |
| @override | |
| Widget build(BuildContext context, WidgetRef ref) { | |
| final addTodo = ref.watch(addTodoProvider); | |
| ref.listen(addTodoProvider, (prev, next) { | |
| next.whenOrNull( | |
| error: (error, stackTrace) { | |
| showDialog<void>( | |
| context: context, | |
| builder: (context) => AlertDialog( | |
| title: const Text('Error'), | |
| content: Text(error.toString()), | |
| actions: [ | |
| TextButton( | |
| onPressed: () => Navigator.of(context).pop(), | |
| child: const Text('OK'), | |
| ), | |
| ], | |
| ), | |
| ); | |
| }, | |
| ); | |
| }); | |
| return ListTile( | |
| leading: addTodo.when( | |
| loading: () => const CircularProgressIndicator(), | |
| error: (e, s) => const Icon(Icons.error), | |
| data: (_) => const Icon(Icons.check), | |
| initial: () => const Icon(Icons.add), | |
| ), | |
| title: const Text('Add Todo'), | |
| onTap: addTodo.maybeWhen( | |
| orElse: () => () => addTodo(Todo( | |
| title: 'NEW Todo ${DateTime.now().second}', | |
| )), | |
| loading: () => null, | |
| ), | |
| ); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | |
| import 'mutation_state.dart'; | |
| // make sure consumers see the extension | |
| export 'mutation_state.dart'; | |
| part 'mutation_provider.g.dart'; | |
| // generic mutation. pass a function into call to execute the mutation | |
| // the mutationKey will allow creating a new provider per context | |
| // allows you to avoid creating a new mutation definition for every single function | |
| @riverpod | |
| MutationState<void, Future<void> Function()> mutation(MutationRef ref, Object mutationKey) { | |
| // simply calls the function provided by the user | |
| return MutationState.create(ref, (fn) => fn()); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:meta/meta.dart'; | |
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | |
| import 'package:stack_trace/stack_trace.dart'; | |
| /// AsyncValue-like data class, but includes a ref and callback | |
| /// strictly for use in providers | |
| /// `_fn` is the handler for whatever parameter gets passed in (a call to a notifier function, for example) | |
| /// Param is the type of the value that gets passed in by the front end | |
| /// T is the type of the created data from the call (equivalent to AsyncValue's T) | |
| /// (the type of the `value`) | |
| @immutable | |
| abstract class MutationState<T, Param> { | |
| const MutationState._(this._ref, this._fn); | |
| const factory MutationState._initial( | |
| ProviderRef<MutationState<T, Param>> _ref, | |
| Future<T> Function(Param p) _fn, | |
| ) = MutationInitial<T, Param>._; | |
| factory MutationState.create( | |
| ProviderRef<MutationState<T, Param>> _ref, | |
| Future<T> Function(Param p) _fn, | |
| ) { | |
| return MutationState<T, Param>._initial(_ref, _fn); | |
| } | |
| const factory MutationState._error( | |
| ProviderRef<MutationState<T, Param>> _ref, | |
| Future<T> Function(Param p) _fn, | |
| Object error, { | |
| required StackTrace stackTrace, | |
| }) = MutationError<T, Param>._; | |
| const factory MutationState._data( | |
| ProviderRef<MutationState<T, Param>> _ref, | |
| Future<T> Function(Param p) _fn, | |
| T value, | |
| ) = MutationData<T, Param>._; | |
| const factory MutationState._loading( | |
| ProviderRef<MutationState<T, Param>> _ref, | |
| Future<T> Function(Param p) _fn, | |
| ) = MutationLoading<T, Param>._; | |
| static Future<MutationState<T, P>> _guard<T, P>( | |
| ProviderRef<MutationState<T, P>> _ref, | |
| Future<T> Function(P p) cb, | |
| P parameter, | |
| ) async { | |
| try { | |
| return MutationState<T, P>._data(_ref, cb, await cb(parameter)); | |
| } catch (e, s) { | |
| return MutationState<T, P>._error(_ref, cb, e, stackTrace: s); | |
| } | |
| } | |
| final ProviderRef<MutationState<T, Param>> _ref; | |
| bool get isLoading; | |
| bool get hasValue; | |
| T? get value; | |
| Object? get error; | |
| StackTrace? get stackTrace; | |
| final Future<T> Function(Param p) _fn; | |
| void _setState(MutationState<T, Param> state) { | |
| _ref.state = state.copyWithPrevious(_ref.state); | |
| } | |
| Future<MutationState<T, Param>> call(Param parameter) async { | |
| final cb = this._fn; | |
| _setState(MutationState<T, Param>._loading(_ref, cb)); | |
| final result = await MutationState._guard<T, Param>(_ref, cb, parameter); | |
| _setState(result); | |
| return result; | |
| } | |
| R map<R>({ | |
| required R Function(MutationInitial<T, Param> initial) initial, | |
| required R Function(MutationData<T, Param> data) data, | |
| required R Function(MutationError<T, Param> error) error, | |
| required R Function(MutationLoading<T, Param> loading) loading, | |
| }); | |
| MutationState<T, Param> copyWithPrevious(MutationState<T, Param> previous); | |
| MutationState<T, Param> unwrapPrevious() { | |
| return map( | |
| initial: (i) => MutationInitial<T, Param>._(_ref, _fn), | |
| data: (d) { | |
| if (d.isLoading) return MutationLoading<T, Param>._(_ref, _fn); | |
| return MutationData._(_ref, _fn, d.value!); | |
| }, | |
| error: (e) { | |
| if (e.isLoading) return MutationLoading._(_ref, _fn); | |
| return MutationError._(_ref, _fn, e.error, stackTrace: e.stackTrace); | |
| }, | |
| loading: (l) => MutationLoading<T, Param>._(_ref, _fn), | |
| ); | |
| } | |
| @override | |
| String toString() { | |
| final content = [ | |
| if (isLoading && this is! MutationLoading) 'isLoading: $isLoading', | |
| if (hasValue) 'value: $value', | |
| if (hasError) ...[ | |
| 'error: $error', | |
| 'stackTrace: $stackTrace', | |
| ], | |
| if (stackTrace != null) 'stackTrace: $stackTrace', | |
| ].join(', '); | |
| return '$runtimeType($content)'; | |
| } | |
| @override | |
| bool operator ==(Object other) { | |
| return runtimeType == other.runtimeType && | |
| other is MutationState<T, Param> && | |
| other.isInitial == isInitial && | |
| other.isLoading == isLoading && | |
| other.hasValue == hasValue && | |
| other.error == error && | |
| other.stackTrace == stackTrace && | |
| other.valueOrNull == valueOrNull; | |
| } | |
| @override | |
| int get hashCode => Object.hash( | |
| runtimeType, | |
| isLoading, | |
| hasValue, | |
| // Fallback null values to 0, making sure Object.hash hashes all values | |
| valueOrNull ?? 0, | |
| error ?? 0, | |
| stackTrace ?? 0, | |
| ); | |
| } | |
| class MutationInitial<T, F> extends MutationState<T, F> { | |
| const MutationInitial._(super._ref, super._fn) : super._(); | |
| @override | |
| T? get value => null; | |
| @override | |
| bool get hasValue => false; | |
| @override | |
| bool get isLoading => false; | |
| @override | |
| StackTrace? get stackTrace => null; | |
| @override | |
| Object? get error => null; | |
| @override | |
| R map<R>( | |
| {required R Function(MutationInitial<T, F> initial) initial, | |
| required R Function(MutationData<T, F> data) data, | |
| required R Function(MutationError<T, F> error) error, | |
| required R Function(MutationLoading<T, F> loading) loading}) { | |
| return initial(this); | |
| } | |
| @override | |
| MutationState<T, F> copyWithPrevious(MutationState<T, F> previous) { | |
| // We shouldn't even have a previous value if we are initial | |
| return this; | |
| } | |
| } | |
| class MutationLoading<T, F> extends MutationState<T, F> { | |
| const MutationLoading._(super._ref, super._fn) | |
| : hasValue = false, | |
| value = null, | |
| error = null, | |
| stackTrace = null, | |
| super._(); | |
| const MutationLoading.__( | |
| super._ref, | |
| super._fn, { | |
| required this.hasValue, | |
| required this.value, | |
| required this.error, | |
| required this.stackTrace, | |
| }) : super._(); | |
| @override | |
| bool get isLoading => true; | |
| @override | |
| final bool hasValue; | |
| @override | |
| final T? value; | |
| @override | |
| final Object? error; | |
| @override | |
| final StackTrace? stackTrace; | |
| @override | |
| R map<R>({ | |
| required R Function(MutationInitial<T, F> initial) initial, | |
| required R Function(MutationData<T, F> data) data, | |
| required R Function(MutationError<T, F> error) error, | |
| required R Function(MutationLoading<T, F> loading) loading, | |
| }) { | |
| return loading(this); | |
| } | |
| @override | |
| MutationState<T, F> copyWithPrevious( | |
| MutationState<T, F> previous, { | |
| bool isRefresh = true, | |
| }) { | |
| if (isRefresh) { | |
| return previous.map( | |
| initial: (_) => this, | |
| data: (d) => MutationData.__( | |
| _ref, | |
| _fn, | |
| d.value, | |
| isLoading: true, | |
| error: d.error, | |
| stackTrace: d.stackTrace, | |
| ), | |
| error: (e) => MutationError.__( | |
| _ref, | |
| _fn, | |
| e.error, | |
| isLoading: true, | |
| value: e.valueOrNull, | |
| stackTrace: e.stackTrace, | |
| hasValue: e.hasValue, | |
| ), | |
| loading: (_) => this, | |
| ); | |
| } else { | |
| return previous.map( | |
| initial: (_) => this, | |
| data: (e) => MutationLoading.__( | |
| _ref, | |
| _fn, | |
| hasValue: true, | |
| value: e.valueOrNull, | |
| error: e.error, | |
| stackTrace: e.stackTrace, | |
| ), | |
| error: (e) => MutationLoading.__( | |
| _ref, | |
| _fn, | |
| hasValue: e.hasValue, | |
| value: e.valueOrNull, | |
| error: e.error, | |
| stackTrace: e.stackTrace, | |
| ), | |
| loading: (e) => e, | |
| ); | |
| } | |
| } | |
| } | |
| class MutationData<T, F> extends MutationState<T, F> { | |
| const MutationData._(super._ref, super._fn, this.value) | |
| : isLoading = false, | |
| error = null, | |
| stackTrace = null, | |
| super._(); | |
| const MutationData.__( | |
| super._ref, | |
| super._fn, | |
| this.value, { | |
| required this.isLoading, | |
| required this.error, | |
| required this.stackTrace, | |
| }) : super._(); | |
| @override | |
| final T value; | |
| @override | |
| bool get hasValue => true; | |
| @override | |
| final bool isLoading; | |
| @override | |
| final Object? error; | |
| @override | |
| final StackTrace? stackTrace; | |
| @override | |
| R map<R>({ | |
| required R Function(MutationInitial<T, F> initial) initial, | |
| required R Function(MutationData<T, F> data) data, | |
| required R Function(MutationError<T, F> error) error, | |
| required R Function(MutationLoading<T, F> loading) loading, | |
| }) { | |
| return data(this); | |
| } | |
| @override | |
| MutationState<T, F> copyWithPrevious(MutationState<T, F> previous) => this; | |
| } | |
| class MutationError<T, F> extends MutationState<T, F> { | |
| const MutationError.__( | |
| super._ref, | |
| super._fn, | |
| this.error, { | |
| required this.stackTrace, | |
| required this.isLoading, | |
| required T? value, | |
| required this.hasValue, | |
| }) : _value = value, | |
| super._(); | |
| const MutationError._(super._ref, super._fn, this.error, | |
| {required this.stackTrace}) | |
| : isLoading = false, | |
| hasValue = false, | |
| _value = null, | |
| super._(); | |
| @override | |
| final bool isLoading; | |
| @override | |
| final bool hasValue; | |
| @override | |
| T? get value { | |
| if (!hasValue) { | |
| throwErrorWithCombinedStackTrace(error, stackTrace); | |
| } | |
| return _value; | |
| } | |
| final T? _value; | |
| @override | |
| final Object error; | |
| @override | |
| final StackTrace stackTrace; | |
| @override | |
| R map<R>({ | |
| required R Function(MutationInitial<T, F> initial) initial, | |
| required R Function(MutationData<T, F> data) data, | |
| required R Function(MutationError<T, F> error) error, | |
| required R Function(MutationLoading<T, F> loading) loading, | |
| }) { | |
| return error(this); | |
| } | |
| @override | |
| MutationState<T, F> copyWithPrevious(MutationState<T, F> previous) { | |
| return MutationError.__( | |
| _ref, | |
| _fn, | |
| error, | |
| stackTrace: stackTrace, | |
| isLoading: isLoading, | |
| value: previous.valueOrNull, | |
| hasValue: previous.hasValue, | |
| ); | |
| } | |
| } | |
| extension MutationValueX<T, F> on MutationState<T, F> { | |
| bool get isInitial => this is MutationInitial; | |
| T get requireValue { | |
| if (hasValue) return value as T; | |
| if (hasError) throwErrorWithCombinedStackTrace(error!, stackTrace!); | |
| throw StateError( | |
| 'Tried to call `requireValue` on a `MutationValue`' | |
| ' that has no value: $this', | |
| ); | |
| } | |
| T? get valueOrNull => hasValue ? value : null; | |
| bool get isRerunning => | |
| isLoading && (hasValue || hasError) && this is! MutationLoading; | |
| bool get hasError => error != null; | |
| MutationData<T, F>? get asData => | |
| maybeMap(data: (d) => d, orElse: () => null); | |
| MutationError<T, F>? get asError => | |
| maybeMap(error: (e) => e, orElse: () => null); | |
| R maybeWhen<R>({ | |
| bool skipLoadingOnRerun = false, | |
| bool skipError = false, | |
| R Function()? initial, | |
| R Function(T value)? data, | |
| R Function(Object error, StackTrace stackTrace)? error, | |
| R Function()? loading, | |
| required R Function() orElse, | |
| }) { | |
| return when( | |
| skipError: skipError, | |
| skipLoadingOnRerun: skipLoadingOnRerun, | |
| initial: initial ?? orElse, | |
| loading: loading ?? orElse, | |
| data: (d) { | |
| if (data != null) return data(d); | |
| return orElse(); | |
| }, | |
| error: (e, s) { | |
| if (error != null) return error(e, s); | |
| return orElse(); | |
| }, | |
| ); | |
| } | |
| R when<R>({ | |
| bool skipLoadingOnRerun = false, | |
| bool skipError = false, | |
| required R Function() initial, | |
| required R Function(T data) data, | |
| required R Function(Object error, StackTrace stackTrace) error, | |
| required R Function() loading, | |
| }) { | |
| if (isInitial) { | |
| return initial(); | |
| } | |
| if (isLoading) { | |
| bool skip; | |
| if (isRerunning) { | |
| skip = skipLoadingOnRerun; | |
| } else { | |
| skip = false; | |
| } | |
| if (!skip) return loading(); | |
| } | |
| if (hasError && (!hasValue || !skipError)) { | |
| return error(this.error!, stackTrace!); | |
| } | |
| return data(requireValue); | |
| } | |
| R? whenOrNull<R>({ | |
| bool skipLoadingOnRerun = false, | |
| bool skipError = false, | |
| R? Function()? initial, | |
| R? Function(T data)? data, | |
| R? Function(Object error, StackTrace stackTrace)? error, | |
| R? Function()? loading, | |
| }) { | |
| return when( | |
| skipError: skipError, | |
| skipLoadingOnRerun: skipLoadingOnRerun, | |
| initial: initial ?? () => null, | |
| data: data ?? (_) => null, | |
| error: error ?? (err, stack) => null, | |
| loading: loading ?? () => null, | |
| ); | |
| } | |
| R maybeMap<R>({ | |
| R Function(MutationInitial<T, F> initial)? initial, | |
| R Function(MutationData<T, F> data)? data, | |
| R Function(MutationError<T, F> error)? error, | |
| R Function(MutationLoading<T, F> loading)? loading, | |
| required R Function() orElse, | |
| }) { | |
| return map( | |
| initial: (i) { | |
| if (initial != null) return initial(i); | |
| return orElse(); | |
| }, | |
| data: (d) { | |
| if (data != null) return data(d); | |
| return orElse(); | |
| }, | |
| error: (e) { | |
| if (error != null) return error(e); | |
| return orElse(); | |
| }, | |
| loading: (l) { | |
| if (loading != null) return loading(l); | |
| return orElse(); | |
| }, | |
| ); | |
| } | |
| } | |
| Never throwErrorWithCombinedStackTrace(Object error, StackTrace stackTrace) { | |
| final chain = Chain([ | |
| Trace.current(), | |
| ...Chain.forTrace(stackTrace).traces, | |
| ]).foldFrames((frame) => frame.package == 'riverpod'); | |
| Error.throwWithStackTrace(error, chain); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment