Last active
July 3, 2025 15:58
-
-
Save HenriqueNas/dedb6e6523f1ba03acedf780d2c89ef9 to your computer and use it in GitHub Desktop.
Easiest way to scale your management of your Flutter page/widget state without any package.
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:async'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/widgets.dart'; | |
/// Defines the possible states of a UI page or widget. | |
/// | |
/// This enum helps to clearly represent different stages of a UI, | |
/// such as waiting for data (`loading`), displaying content (`success`), | |
/// or indicating an issue (`error`). | |
enum PageState { | |
/// The initial or default state. | |
/// No operation is currently in progress, and no error has occurred. | |
idle, | |
/// Indicates that an asynchronous operation is in progress, | |
/// such as fetching data from an API. | |
loading, | |
/// Indicates that an error has occurred during an operation. | |
/// An associated error object can provide more details. | |
error, | |
/// Indicates that an operation has completed successfully. | |
success; | |
/// Returns `true` if the current state is [PageState.loading]. | |
bool get isLoading => this == loading; | |
/// Returns `true` if the current state is [PageState.error]. | |
bool get isError => this == error; | |
/// Returns `true` if the current state is [PageState.success]. | |
bool get isSuccess => this == success; | |
/// Returns `true` if the current state is [PageState.idle]. | |
bool get isIdle => this == idle; | |
} | |
/// A `ChangeNotifier` that helps manage and communicate the state | |
/// of a UI page or widget throughout its lifecycle. | |
/// | |
/// This class simplifies state management by providing a clear way | |
/// to set and react to different `PageState` values. It's ideal for | |
/// scenarios where a UI needs to respond to asynchronous operations | |
/// (e.g., data loading, form submissions) by displaying appropriate | |
/// visual feedback (loading indicators, error messages, success views). | |
/// | |
/// Type parameter `Error`: The type of the error object that can be | |
/// stored when the state is [PageState.error]. | |
/// | |
/// Example Usage: | |
/// ```dart | |
/// class MyPage extends StatefulWidget { | |
/// @override | |
/// _MyPageState createState() => _MyPageState(); | |
/// } | |
/// | |
/// class _MyPageState extends State<MyPage> { | |
/// final pageStateNotifier = PageStateNotifier<String>(); // Error type is String | |
/// | |
/// @override | |
/// void initState() { | |
/// super.initState(); | |
/// // Listen for state changes to perform side effects | |
/// pageStateNotifier.onSuccess(() { | |
/// ScaffoldMessenger.of(context).showSnackBar( | |
/// SnackBar(content: Text('Operation successful!')), | |
/// ); | |
/// }); | |
/// pageStateNotifier.onError((error) { | |
/// ScaffoldMessenger.of(context).showSnackBar( | |
/// SnackBar(content: Text('Error: $error')), | |
/// ); | |
/// }); | |
/// } | |
/// | |
/// Future<void> _fetchData() async { | |
/// return Future.value(); | |
/// } | |
/// | |
/// @override | |
/// Widget build(BuildContext context) { | |
/// return Scaffold( | |
/// appBar: AppBar(title: Text('State Notifier Example')), | |
/// body: Center( | |
/// child: Column( | |
/// mainAxisAlignment: MainAxisAlignment.center, | |
/// children: [ | |
/// // Use stateBuilder to render different UI based on the PageState | |
/// pageStateNotifier.stateBuilder( | |
/// idle: () => Text('Press button to load data'), | |
/// loading: () => CircularProgressIndicator(), | |
/// success: () => Text('Data loaded!'), | |
/// error: (error) => Text('Failed: $error'), | |
/// ), | |
/// SizedBox(height: 20), | |
/// _pageDataNotifier.builder((state) { | |
/// return ElevatedButton( | |
/// onPressed: state.isLoading ? null : _fetchData, | |
/// child: state.isLoading ? Text('Loading...') : Text('Load Data'), | |
/// ); | |
/// }); | |
/// ], | |
/// ), | |
/// ), | |
/// ); | |
/// } | |
/// | |
/// @override | |
/// void dispose() { | |
/// pageStateNotifier.dispose(); | |
/// super.dispose(); | |
/// } | |
/// } | |
/// ``` | |
class PageStateNotifier<Error extends Object?> extends ChangeNotifier { | |
PageState _state = PageState.idle; | |
/// The current [PageState] of the notifier. | |
/// | |
/// This getter provides the current state, which should be used | |
/// to control the UI's appearance and behavior. | |
PageState get state => _state; | |
void _setState(final PageState state) { | |
_state = state; | |
notifyListeners(); | |
} | |
/// Sets the current state to [PageState.success] and notifies | |
/// all registered listeners, triggering UI updates. | |
void setSuccess() => _setState(PageState.success); | |
/// Returns `true` if the current [state] is [PageState.success]. | |
bool get isSuccess => _state.isSuccess; | |
Error? _error; | |
/// Sets an error object and transitions the state to [PageState.error]. | |
/// | |
/// This method notifies all registered listeners, enabling the UI | |
/// to display error messages or appropriate feedback. | |
/// | |
/// `error`: An optional error object. If not provided, the existing | |
/// error (if any) will remain, or it will be set to `null`. | |
void setError([final Error? error]) { | |
_error = error; | |
_setState(PageState.error); | |
} | |
/// The error object associated with the [PageState.error] state. | |
/// | |
/// If the current `state` is [PageState.error], this getter returns | |
/// the error object that was set. Otherwise, it returns `null`. | |
Error? get error => _error; | |
/// Returns `true` if the current [state] is [PageState.error]. | |
bool get isError => _state.isError; | |
/// Returns `true` if the current [state] is [PageState.idle]. | |
bool get isIdle => _state.isIdle; | |
/// Returns `true` if the current [state] is [PageState.loading]. | |
bool get isLoading => _state.isLoading; | |
/// Sets the current state to [PageState.idle] and notifies | |
/// all registered listeners. | |
/// | |
/// Use this to reset the state to its initial or inactive status. | |
void setIdle() => _setState(PageState.idle); | |
/// Sets the current state to [PageState.loading] and notifies | |
/// all registered listeners. | |
/// | |
/// Use this when an asynchronous operation begins. | |
void setLoading() => _setState(PageState.loading); | |
/// Registers a callback to be executed specifically when the | |
/// `state` changes to [PageState.error]. | |
/// | |
/// The provided `callback` will receive the current `error` object. | |
/// This is useful for displaying error messages or performing error-specific | |
/// actions. | |
/// | |
/// `callback`: A function that takes an `Error?` object. | |
void onError(final void Function([Error? error]) callback) => addListener(() { | |
if (isError) callback(error); | |
}); | |
/// Registers a callback to be executed specifically when the | |
/// `state` changes to [PageState.success]. | |
/// | |
/// This is useful for navigating, showing success messages, or | |
/// performing other actions upon successful completion. | |
/// | |
/// `callback`: A function that takes no arguments. | |
void onSuccess(final VoidCallback callback) => addListener(() { | |
if (isSuccess) callback(); | |
}); | |
/// Registers a callback to be executed specifically when the | |
/// `state` changes to [PageState.loading]. | |
/// | |
/// This is useful for displaying loading indicators or preventing | |
/// multiple concurrent operations. | |
/// | |
/// `callback`: A function that takes no arguments. | |
void onLoading(final VoidCallback callback) => addListener(() { | |
if (isLoading) callback(); | |
}); | |
/// Registers a callback to be executed specifically when the | |
/// `state` changes to [PageState.idle]. | |
/// | |
/// This can be used to reset UI elements or perform actions | |
/// when the page returns to an inactive state. | |
/// | |
/// `callback`: A function that takes no arguments. | |
void onIdle(final VoidCallback callback) => addListener(() { | |
if (isIdle) callback(); | |
}); | |
/// Creates a [ListenableBuilder] that rebuilds its UI whenever | |
/// the `PageStateNotifier`'s `state` changes. | |
/// | |
/// This is a generic builder that provides the current `PageState` | |
/// to its `builder` function, allowing for flexible UI construction. | |
/// | |
/// `builder`: A function that receives the current [PageState] | |
/// and returns a [Widget]. | |
Widget builder(Widget Function(PageState state) builder) => ListenableBuilder( | |
listenable: this, | |
builder: (final context, final _) => builder(state), | |
); | |
/// Creates a [ListenableBuilder] that renders different widgets | |
/// based on the current [PageState]. | |
/// | |
/// This method provides a convenient way to define separate UI components | |
/// for each state ([idle], [loading], [success], [error]), making it | |
/// easy to manage complex UI flows. | |
/// | |
/// `idle`: A widget builder for the [PageState.idle] state. Defaults | |
/// to `SizedBox.shrink()`. | |
/// `loading`: An optional widget builder for the [PageState.loading] state. | |
/// `success`: An optional widget builder for the [PageState.success] state. | |
/// `error`: An optional widget builder for the [PageState.error] state. | |
/// It receives the current `error` object. | |
/// `fallbackState`: The state to fall back to if a specific state's | |
/// widget builder is not provided and that state becomes active. | |
/// Assertions ensure that a widget builder is provided if `fallbackState` | |
/// is set to `loading`, `error`, or `success`. | |
/// | |
/// Example: | |
/// ```dart | |
/// pageStateNotifier.stateBuilder( | |
/// idle: () => Text('Ready to start'), | |
/// loading: () => CircularProgressIndicator(), | |
/// success: () => Icon(Icons.check_circle), | |
/// error: (e) => Text('Error: ${e ?? 'Unknown error'}'), | |
/// ); | |
/// ``` | |
Widget stateBuilder({ | |
final Widget Function() idle = SizedBox.shrink, | |
final Widget Function()? loading, | |
final Widget Function()? success, | |
final Widget Function(Error? error)? error, | |
final PageState fallbackState = PageState.idle, | |
}) { | |
assert( | |
!fallbackState.isLoading || loading != null, | |
'Your fallbackState is loading, but you did not provide a loading widget', | |
); | |
assert( | |
!fallbackState.isError || error != null, | |
'Your fallbackState is error, but you did not provide an error widget', | |
); | |
assert( | |
!fallbackState.isSuccess || success != null, | |
'Your fallbackState is success, but you did not provide a success widget', | |
); | |
return ListenableBuilder( | |
listenable: this, | |
builder: (final context, final _) { | |
final widgetBuilder = switch (_state) { | |
PageState.idle => idle(), | |
PageState.loading => loading?.call(), | |
PageState.success => success?.call(), | |
PageState.error => error?.call(_error), | |
}; | |
if (widgetBuilder == null) { | |
return switch (fallbackState) { | |
PageState.idle => idle(), | |
PageState.loading => loading!.call(), | |
PageState.error => error!.call(_error), | |
PageState.success => success!.call(), | |
}; | |
} | |
return widgetBuilder; | |
}, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
other utils
ComputedNotifier:
ComputedNotifier
lets you combine the values of multiple listenables into a newValueListenable
that can be reacted to, or even used by additionalComputedNotifier
.