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; | |
}, | |
); | |
} | |
} |
other utils
ComputedNotifier:
ComputedNotifier
lets you combine the values of multiple listenables into a new ValueListenable
that can be reacted to, or even used by additional ComputedNotifier
.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
/// Provides utility methods for any object that implements [Listenable].
///
/// This extension simplifies common operations with listenable objects,
/// especially in the context of Flutter widgets.
extension ListenableUtils on Listenable {
/// Creates a [ListenableBuilder] that rebuilds its UI whenever this
/// [Listenable] notifies its listeners.
///
/// This is a convenient way to react to changes from any [Listenable]
/// (e.g., `ChangeNotifier`, `AnimationController`) and update a part
/// of the widget tree.
///
/// Example:
/// ```dart
/// final myNotifier = ChangeNotifier();
/// // In your build method:
/// myNotifier.builder(() {
/// return Text('Notifier updated!');
/// });
/// ```
///
/// `builder`: A function that returns the [Widget] to be built
/// when the listenable changes.
Widget builder(final Widget Function() builder) {
return ListenableBuilder(
listenable: this,
builder: (context, _) {
return builder();
},
);
}
/// Creates a [ComputedNotifier] that observes changes in this [Listenable]
/// and recomputes a derived value.
///
/// This is useful for creating "computed" or "derived" states that
/// automatically update when their dependencies change. The `value`
/// of the returned `ComputedNotifier` will be re-evaluated whenever
/// this `Listenable` notifies.
///
/// Example:
/// ```dart
/// final counter = ValueNotifier<int>(0);
/// final isEven = counter.computed(() => counter.value % 2 == 0);
///
/// // In your build method:
/// isEven.builder((value) => Text('Is counter even? ${value.toString()}'));
/// // When counter changes, isEven will automatically recompute and update.
/// ```
///
/// `value`: A function that computes the derived value `T`. This function
/// will be re-executed when this `Listenable` changes.
///
/// Returns a [ComputedNotifier] of type `T`.
ValueListenable computed<T>(T Function() value) {
return ComputedNotifier<T>([this], value);
}
}
/// A [ValueListenable] that recomputes its value based on changes
/// in a list of other [Listenable] dependencies.
///
/// `ComputedNotifier` acts like a reactive variable. When any of its
/// registered `listenableList` dependencies notify a change, it re-evaluates
/// its `_compute` function. If the recomputed value is different from
/// the previous one, it then notifies its own listeners.
/// This is a powerful pattern for creating derived state that automatically
/// stays in sync with its sources.
///
/// Type parameter `T`: The type of the value that this `ComputedNotifier` holds.
class ComputedNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
/// Creates a `ComputedNotifier`.
///
/// `listenableList`: A list of [Listenable] objects that this
/// `ComputedNotifier` will observe. Any change in these listenables
/// will trigger a re-computation of this notifier's value.
/// `_compute`: A function that defines how the value of this notifier
/// is derived. This function is called initially and then whenever
/// any of the `listenableList` dependencies change.
ComputedNotifier(List<Listenable> listenableList, this._compute)
: _currentValue = _compute() {
_listenable = Listenable.merge(listenableList);
_listenable.addListener(_onDependenciesChanged);
// Enables Flutter DevTools memory allocation tracking if available.
if (kFlutterMemoryAllocationsEnabled) {
ChangeNotifier.maybeDispatchObjectCreation(this);
}
}
/// The function used to compute the value of this notifier.
/// This function will be called whenever a dependency changes.
final T Function() _compute;
/// A merged [Listenable] that aggregates all the individual listenables
/// provided in the constructor.
late final Listenable _listenable;
/// The current value held by this notifier.
/// This value is updated when `_compute` is re-evaluated and its
/// result differs from the previous value.
late T _currentValue;
/// Determines if the notifier should notify its listeners after
/// a dependency change.
///
/// This check prevents unnecessary rebuilds by only notifying if the
/// `_compute` function returns a new value.
bool get _shouldNotify {
final T newValue = _compute();
return newValue != _currentValue;
}
/// Callback function executed when any of the observed dependencies change.
///
/// It recomputes the value and notifies listeners only if the value
/// has actually changed.
void _onDependenciesChanged() {
if (_shouldNotify) {
_currentValue = _compute();
notifyListeners();
}
}
@override
/// The current value of this computed notifier.
///
/// This value is derived from the `_compute` function and is updated
/// automatically when dependencies change.
T get value => _currentValue;
@override
/// Disposes the `ComputedNotifier` by removing its listener from
/// the aggregated [Listenable].
///
/// It's crucial to call this method when the `ComputedNotifier` is no
/// longer needed to prevent memory leaks.
void dispose() {
_listenable.removeListener(_onDependenciesChanged);
super.dispose();
}
@override
/// A string representation of the `ComputedNotifier`, including its
/// current value for debugging purposes.
String toString() => '${describeIdentity(this)}($value)';
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
a mixin aggregation example:
you can reuse
UserMixin
in any class that extendsPageStateNotifier
,and any controllers can have many mixins it needs
with multiples mixins: