Clearing cache and reacting to state disposal
So far, we've seen how to create/update some state. But we have yet to talk about when state destruction occurs.
Riverpod offers various ways to interact with state disposal. This ranges from delaying the disposal of state to reacting to destruction.
When is state destroyed and how to change this?
When using code-generation, by default, the state is destroyed when
the provider stops being listened to.
This happens when a listener has no active listener for a full frame.
When that happens, the state is destroyed.
This behavior can be opted out by using keepAlive: true
.
Doing so will prevent the state from getting destroyed when all listeners
are removed.
// We can specify "keepAlive" in the annotation to disable
// the automatic state destruction
(keepAlive: true)
int example(Ref ref) {
return 0;
}
Enabling/disabling automatic disposal has no impact on whether or not
the state is destroyed when the provider is recomputed.
The state will always be destroyed when the provider is recomputed.
When providers receive parameters, it is recommended to enable automatic disposal. That is because otherwise, one state per parameter combination will be created, which can lead to memory leaks.
Reacting to state disposal
In Riverpod, there are a few built-in ways for state to be destroyed:
- The provider is no longer used and is in "auto dispose" mode (more on that later). In this case, all associated state with the provider is destroyed.
- The provider is recomputed, such as with
ref.watch
. In that case, the previous state is disposed, and a new state is created.
In both cases. you may want to execute some logic when that happens.
This can be achieved with ref.onDispose
. This methods enables
registering a listener to whenever the state is destroyed.
For example, you may want use it to close any active StreamController
:
Stream<int> example(Ref ref) {
final controller = StreamController<int>();
// When the state is destroyed, we close the StreamController.
ref.onDispose(controller.close);
// TO-DO: Push some values in the StreamController
return controller.stream;
}
The callback of ref.onDispose
must not trigger side-effects.
Modifying providers inside onDispose
could lead to unexpected behavior.
There are other useful life-cycles such as:
ref.onCancel
which is called when the last listener of a provider is removed.ref.onResume
which is called when a new listener is added afteronCancel
was invoked.
You can call ref.onDispose
as many times as you wish.
Feel free to call it once per disposable object in your provider. This practice
makes it easier to spot when we forget to dispose of something.
Manually forcing the destruction of a provider, using ref.invalidate
Sometimes, you may want to force the destruction of a provider.
This can be done by using ref.invalidate
, which can be called from another
provider or a widget.
Using ref.invalidate
will destroy the current provider state.
There are then two possible outcomes:
- If the provider is listened to, a new state will be created.
- If the provider is not listened to, the provider will be fully destroyed.
class MyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// On click, destroy the provider.
ref.invalidate(someProvider);
},
child: const Text('dispose a provider'),
);
}
}
It is possible for providers to invalidate themselves by using ref.invalidateSelf
.
Although in this case, this will always result in a new state being created.
When trying to invalidate a provider which receives parameters, it is posible to either invalidate one specific parameter combination, or all parameter combinations at once:
String label(Ref ref, String userName) {
return 'Hello $userName';
}
// ...
void onTap() {
// Invalidate all possible parameter combinations of this provider.
ref.invalidate(labelProvider);
// Invalidate a specific combination only
ref.invalidate(labelProvider('John'));
}
Fine-tuned disposal with ref.keepAlive
As mentioned above, when automatic disposal is enabled, the state is destroyed when the provider has no listeners for a full frame.
But you may want to have more control over this behavior. For instance, you may want to keep the state of successful network requests, but not cache failed requests.
This can be achieved with ref.keepAlive
, after enabling automatic disposal.
Using it, you can decide when the state stops being automatically disposed.
Future<String> example(Ref ref) async {
final response = await http.get(Uri.parse('https://example.com'));
// We keep the provider alive only after the request has successfully completed.
// If the request failed (and threw an exception), then when the provider stops being
// listened to, the state will be destroyed.
ref.keepAlive();
// We can use the `link` to restore the auto-dispose behavior with:
// link.close();
return response.body;
}
If the provider is recomputed, automatic disposal will be re-enabled.
It is also possible to use the return value of ref.keepAlive
to
revert to automatic disposal.
Example: keeping state alive for a specific amount of time
Currently, Riverpod does not offer a built-in way to keep state alive
for a specific amount of time.
But implementing such a feature is easy and reusable with the tools we've seen so far.
By using a Timer
+ ref.keepAlive
, we can keep the state alive for a specific amount of time.
To make this logic reusable, we could implement it in an extension method:
extension CacheForExtension on Ref {
/// Keeps the provider alive for [duration].
void cacheFor(Duration duration) {
// Immediately prevent the state from getting destroyed.
final link = keepAlive();
// After duration has elapsed, we re-enable automatic disposal.
final timer = Timer(duration, link.close);
// Optional: when the provider is recomputed (such as with ref.watch),
// we cancel the pending timer.
onDispose(timer.cancel);
}
}
Then, we can use it like so:
Future<Object> example(Ref ref) async {
/// Keeps the state alive for 5 minutes
ref.cacheFor(const Duration(minutes: 5));
return http.get(Uri.https('example.com'));
}
This logic can be tweaked to fit your needs.
For example, you could use ref.onCancel
/ref.onResume
to destroy the state
only if a provider hasn't been listened to for a specific amount of time.