Hello friends!
In this post, I would like to receive feedback regarding our solution to work with the loading, error, and idle states of data. We worked on this solution together with Mikhail Zotyev (author or Elementary package and GDE) and the Wolt internal team.
What is StatefulValueNotifier?
StatefulValueNotifier<T>
is a class that extends ValueNotifier<ValueState<T>>
, where ValueState<T>
is a sealed class representing the state of a value of type T
. It allows you to handle three primary states:
- Idle: The default state when data is successfully loaded or initialized.
- Loading: Indicates that an asynchronous operation is in progress.
- Error: Represents an error state with an associated exception.
StatefulValueNotifier
allows transitioning between these states and update the UI accordingly. This class also implements StatefulValueListenable
so that the private notifier field can be exposed from the owner as listenable.
Understanding ValueState
The ValueState<T>
sealed class has three implementations:
- IdleValueState: Represents the idle state with an optional value.
- LoadingValueState: Represents the loading state, optionally retaining the current value.
- ErrorValueState: Represents the error state with an associated error object and an optional value.
How to Use StatefulValueNotifier
Let’s walk through an example of how to use StatefulValueNotifier
in a Flutter application.
1. Initialization
First, create an instance of StatefulValueNotifier
with an initial value:
final valueNotifier = StatefulValueNotifier<int>.idle(value: 0);
You can also initialize it in a loading or error state:
// Loading state
final valueNotifier = StatefulValueNotifier<int>.loading();
// Error state with an initial error
final valueNotifier = StatefulValueNotifier<int>.error(Exception('Initial error'));
2. Listening to State Changes
You can listen to state changes by adding a listener:
valueNotifier.addListener(() {
final state = valueNotifier.value;
if (state.isIdle) {
// Handle idle state
print('Idle State: ${state.value}');
} else if (state.isLoading) {
// Handle loading state
print('Loading...');
} else if (state.isError) {
// Handle error state
final errorState = state as ErrorValueState<int>;
print('Error: ${errorState.error}');
}
});
or
valueNotifier.addListener(() {
final state = valueNotifier.value;
switch (state) {
case IdleValueState(value: var value):
// Handle idle state
print('Idle State: $value');
break;
case LoadingValueState():
// Handle loading state
print('Loading...');
break;
case ErrorValueState(error: var error, value: var value):
// Handle error state
print('Error: $error');
break;
}
});
3. Updating the State
- Set to Loading
valueNotifier.setLoading();
Optionally, retain the current value or set a new one:
// Retain current value when switching state
valueNotifier.setLoading(retainValue: true);
// Set a new value
valueNotifier.setLoading(value: 10);
- Set to Error
valueNotifier.setError(error: Exception('An error occurred'));
Optionally, retain the current value or set a new one:
// Retain current value when switching state
valueNotifier.setError(error: Exception('An error occurred'), retainValue: true);
// Set a new value
valueNotifier.setError(error: Exception('An error occurred'), value: 20);
- Set to Idle with a New Value
valueNotifier.setIdle(value: 42);
4. Using with Widgets
You can use StatefulValueListenableBuilder
to rebuild your widgets based on the current state:
StatefulValueListenableBuilder<int>(
valueListenable: valueNotifier,
idleBuilder: (context, value) {
return Text('Idle State. Value: $value');
},
loadingBuilder: (context, value) {
return CircularProgressIndicator();
},
errorBuilder: (context, error, value) {
return Text('Error: $error');
},
);
Complete Example
Below is an example demonstrating how to use StatefulValueNotifier
and StatefulValueListenableBuilder
.
class _MyWidgetState extends State<MyWidget> {
late StatefulValueNotifier<String> jokeNotifier;
@override
void initState() {
super.initState();
// Initialize the value notifier with a loading state
jokeNotifier = StatefulValueNotifier<String>.loading();
// Fetch the first joke
fetchJoke();
}
@override
void dispose() {
jokeNotifier.dispose();
super.dispose();
}
/// Method to fetch a joke
void fetchJoke() {
jokeNotifier.setLoading(retainValue: true);
// Simulate a network request
Future.delayed(Duration(seconds: 2), () async {
try {
// Simulate fetching data from an API
final joke = await _getRandomJoke();
jokeNotifier.setIdle(value: joke);
} catch (e) {
jokeNotifier.setError(error: e, retainValue: true);
}
});
}
@override
Widget build(BuildContext context) {
return StatefulValueListenableBuilder<String>(
valueListenable: jokeNotifier,
idleBuilder: (context, joke) {
return JokeDisplay(
joke: joke ?? 'No joke available',
onRefresh: fetchJoke,
);
},
loadingBuilder: (context, joke) {
return Column(
children: [
CircularProgressIndicator(),
if (joke != null) ...[
SizedBox(height: 16),
Text('Previous joke: $joke'),
],
],
);
},
errorBuilder: (context, error, joke) {
return ErrorDisplay(
error: error,
onRetry: fetchJoke,
);
},
);
}
/// Simulate an API call to get a random joke
Future<String> _getRandomJoke() async {
// Simulate success or failure randomly
if (DateTime.now().second % 2 == 0) {
return 'Why did the programmer quit his job? Because he didn’t get arrays.';
} else {
throw Exception('Failed to fetch joke.');
}
}
}