How to Check if an AsyncNotifier is Mounted with Riverpod

In the previous article, we learned how to check if a widget is mounted after performing some asynchronous work.

An example of this is when we submit some form data and attempt to dismiss the page before the operation is complete:

What happens if we close the page before the operation has completed?
What happens if we close the page before the operation has completed?

As we have seen, we can handle the form submission with a _submitReview method that is called from the onPressed callback of the submit button:

class _LeaveReviewFormState extends ConsumerState<LeaveReviewForm> { // called from a button callback Future<void> _submitReview() async { try { // create a review object with the rating and comment final review = Review(rating: _rating, comment: _controller.text); // obtain the review service from a Riverpod provider final reviewsService = ref.read(reviewsServiceProvider); // use it to submit the review (productId is a property from the widget class) await reviewsService.submitReview(productId: widget.productId, review: review); // check if the widget is still mounted after the async operation if (context.mounted) { // pop the current route (uses GoRouter extension) context.pop(); } } catch (e, st) { // TODO: show alert error } } }

We've also learned that for a better separation of concerns, we can move the business logic into an AsyncNotifier subclass (from the Riverpod package).

Based on the "leave review" example above, we ended up with this code:

class LeaveReviewController extends AutoDisposeAsyncNotifier<void> { @override FutureOr<void> build() { // nothing to do } Future<void> submitReview({ required ProductID productId, required double rating, required String comment, }) async { // create a review object with the rating and comment final review = Review(rating: rating, comment: comment); // obtain the review service from a Riverpod provider final reviewsService = ref.read(reviewsServiceProvider); // set the widget state to loading state = const AsyncLoading(); // submit the review and update the state when done state = await AsyncValue.guard(() => reviewsService.submitReview(productId: productId, review: review)); // TODO: check for mounted if (state.hasError == false) { // get the GoRouter instance and call pop on it ref.read(goRouterProvider).pop(); } } }

This works by getting the GoRouter instance from a provider, so that we can pop the current route:

ref.read(goRouterProvider).pop();

However, if we submit the form and close the page before the asynchronous operation completes, we end up with a "Bad state" error:

Attempting to set the state when the notifier has already been disposed results in this error
Attempting to set the state when the notifier has already been disposed results in this error

To prevent this, we need some way to check if the AsyncNotifier is mounted.

This may seem like a simple question. But as we're about to see, there's no easy answer. And in this article, we'll explore some solutions and their tradeoffs.

Ready? Let's go!

This article assumes you're already familiar with AsyncNotifier. To learn more, read: How to use Notifier and AsyncNotifier with the new Flutter Riverpod Generator

Checking if an AsyncNotifier is mounted

To prevent the "Bad state" error, we could try to write something like this inside our LeaveReviewController:

final newState = await AsyncValue.guard(someFuture); if (mounted) { // Error -> Undefined name 'mounted' // * only set the state if the controller is still mounted state = newState; if (state.hasError == false) { // get the GoRouter instance and call pop on it ref.read(goRouterProvider).pop(); } }

Unfortunately, there's currently no plan to add a mounted property to Notifier/AsyncNotifier, and the code above won't compile.

So let's see what we can do about it. 👇

1. Roll out our own "mounted" property

To focus on the problem at hand, we'll leave the LeaveReviewController aside and use this simple notifier instead:

// just a simple notifier class SomeNotifier extends AutoDisposeAsyncNotifier<void> { @override FutureOr<void> build() { } Future<void> doAsyncWork() async { state = const AsyncLoading(); final newState = await AsyncValue.guard(someFuture); // TODO: Check if mounted, then: state = newState; } } // the corresponding provider final someNotifierProvider = AutoDisposeAsyncNotifierProvider<SomeNotifier, void>( SomeNotifier.new);

As a first attempt, we can try to add a mounted property as a boolean:

class SomeNotifier extends AutoDisposeAsyncNotifier<void> { // 1. initialize to true bool mounted = true; @override FutureOr<void> build() { // 2. set to false on dispose ref.onDispose(() => mounted = false); } Future<void> doAsyncWork() async { state = const AsyncLoading(); final newState = await AsyncValue.guard(someFuture); // 3. check before setting the state if (mounted) { state = newState; } } }

Warning: using a boolean mounted flag is not recommended, and we'll explore a more reliable solution shortly.

The code above will prevent the "Bad state" error by adding a boolean mounted property that:

  1. is set to true on initialization
  2. is set to false on dispose (using the onDispose lifecycle callback)
  3. can be checked after awaiting for some async work to complete (note how newState is only assigned to state if mounted is true)

But we have one crucial question: will the mounted flag inside our notifier be set to false as soon as the widget is unmounted?

And the answer is yes because we are using an AutoDisposeAsyncNotifier. This guarantees that the provider is disposed as soon as the last listener is removed.

To learn more about the Riverpod providers lifecycle, read: Riverpod Data Caching and Providers Lifecycle: Full Guide

While the solution above is not recommended, it appears to work well in practice but it's not very DRY.

Implementing a NotifierMounted mixin

To avoid copy-pasting all the mounted logic to each notifier in our app, we can create a handy mixin:

mixin NotifierMounted { bool _mounted = true; void setUnmounted() => _mounted = false; bool get mounted => _mounted; }

Here's now to use it:

class SomeNotifier extends AutoDisposeAsyncNotifier<void> // 1. add mixin with NotifierMounted { @override FutureOr<void> build() { // 2. set to false on dispose ref.onDispose(setUnmounted); } Future<void> doAsyncWork() async { state = const AsyncLoading(); final newState = await AsyncValue.guard(someFuture); // 3. check before setting the state if (mounted) { state = newState; } } }

I've been using this technique without problems in my projects for a while.

However, I recently came across this issue where Remi explained that using a mounted property is misleading:

If the author of Riverpod himself says not to do something, chances are he knows what he's talking about. 😅

Indeed, if it were possible to add a mounted flag safely, it would have been built into Notifier/AsyncNotifier from the start.

So let's go back to the drawing board. 👇

2. Using Object rather than bool

To solve the "mounted" issue, Remi recommends using an Object. So here's an adaptation of the proposed solution:

class SomeNotifier extends AutoDisposeAsyncNotifier<void> { Object? key; // 1. create a key @override FutureOr<void> build() { key = Object(); // 2. initialize it ref.onDispose(() => key = null); // 3. set to null on dispose } Future<void> doAsyncWork() async { state = const AsyncLoading(); final key = this.key; // 4. grab the key before doing any async work final newState = await AsyncValue.guard(someFuture); if (key == this.key) { // 5. check if the key is still the same state = newState; } } }

This solution is technically correct but requires a total of five steps just to check if the notifier is mounted.

To improve things, I tried moving some logic back into a mixin, but Remi didn't approve my solution.

So how do we move forward?

3. Use a StateNotifier

Since adding mounted to Notifier/AsyncNotifier is a no-no, we could use a good old StateNotifier instead.

So here's an equivalent implementation of the same class:

class SomeNotifier extends StateNotifier<AsyncValue<void>> { SomeNotifier(this.ref) : super(const AsyncData(null)); final Ref ref; Future<void> doAsyncWork() async { state = const AsyncLoading(); final newState = await AsyncValue.guard(someFuture); // mounted is a property and we can use it if needed if (mounted) { state = newState; } } } // the corresponding provider final someNotifierProvider = StateNotifierProvider.autoDispose<SomeNotifier, AsyncValue<void>>( (ref) => SomeNotifier(ref));

As we can see, the syntax is slightly different:

  • we need to provide an initial value to the super constructor, synchronously
  • if we need a ref, we need to declare it explicitly and pass it to the constructor
  • we need to use the autoDispose modifier in the provider declaration

But on the plus side, mounted is already a property of StateNotifier, and we can use it as needed.

Indeed, this is a good way to go as long as you don't need the modern Notifier APIs (asynchronous initialization, multiple arguments, code generation).

Conclusion

We started with a simple question: can we check if an AsyncNotifier is mounted?

Surprisingly, there is no simple answer, and we ended up with three alternatives:

  1. add our own mounted flag inside a NotifierMounted mixin → not recommended
  2. implement some convoluted logic using Object → correct but error-prone and not DRY
  3. use the good old StateNotifier → works well but does not use the modern Notifier syntax

What's next for Riverpod?

Overall, AsyncNotifier is very versatile. But as we have seen, there's no way to check the mounted state, and that can be problematic in practice.

Another area of improvement is support for data mutations, which would further streamline the AsyncNotifier syntax when setting loading/error/result states.

As it turns out, this is a planned feature:

And once this is implemented, I'll be sure to cover it in a future article. 👍

Happy coding!

Flutter Foundations Course Now Available

I launched a brand new course that covers state management with Riverpod in great depth, along with other important topics like app architecture, routing, testing, and much more:

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter In Production

Flutter In Production

Learn about flavors, environments, error monitoring, analytics, release management, CI/CD, and finally ship your Flutter apps to the stores. 🚀

Flutter & Firebase Masterclass

Flutter & Firebase Masterclass

Learn about Firebase Auth, Cloud Firestore, Cloud Functions, Stripe payments, and much more by building a full-stack eCommerce app with Flutter & Firebase.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Last updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.