Flutter Riverpod Handling Future and dispose in StateNotifier

eye-catch Dart and Flutter

It took me a while to find out how to implement a Provider that uses FutureProvider in it and disposes of a variable created in the Provider. You can learn how to implement it in this article.

Sponsored links

Fetch data from database and attach it to TextField

What I want to do is to fetch data from the database and use it for TextField. The following data is an example for the table data.

class MyData {
  final int uid;
  final String name;
  MyData({
    required this.uid,
    required this.name,
  });
}

It’s simple. The next thing is to handle TextEditingController. I need to put the two data into one class because I want to

  • update MyData when TextField is updated
  • assign the initial value to the TextField when canceling the editing
  • put many TextFields

For the third point, those two data need to be in the same class to get the corresponding data.

class _Params {
  final MyData data;
  final TextEditingController controller;

  _Params({
    required this.data,
    required this.controller,
  });

  void dispose() {
    controller.dispose();
    print("Data for uid ${data.uid} was disposed");
  }
}

Let’s prepare a function to fetch data that is async. This function creates MyData list.

Future<List<MyData>> _fetchFromDatabase() => Future.value([0, 1, 2, 3, 4]).then(
    (value) => value.map((e) => MyData(uid: e, name: "name $e")).toList());

It’s ready to implement.

Sponsored links

Implement StateNotifier class

I firstly tried to implement it without StateNotifier. The first code looks like this.

// NG Example
final _provider1 =
    FutureProvider<List<MyData>>((ref) => _fetchFromDatabase());

final _futureProvider = FutureProvider.autoDispose<List<_Params>>((ref) async {
  final value = await ref.watch(_provider1.future);
  final list = value.map((e) {
    return _Params(
      data: e,
      controller: TextEditingController(text: e.name),
    );
  }).toList();

  return list;
});

This works only for adding new items but it doesn’t call dispose function when it’s no longer used because _Params is a normal class that just implements dispose. It is not something inherited from a Base class defined by Riverpod.

To dispose of, we need to implement StateNotifier. The full code looks like this.

class _ParamsListNotifier extends StateNotifier<AsyncValue<List<_Params>>> {
  _ParamsListNotifier() : super(const AsyncValue.loading()) {
    _fetch();
  }
  _ParamsListNotifier.create(List<_Params> state)
      : super(AsyncValue.data(state));

  @override
  void dispose() {
    state.whenData((value) {
      for (final data in value) {
        data.dispose();
      }
    });
    super.dispose();
  }

  Future<void> _fetch() async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      final data = await _fetchFromDatabase();
      return data.map((e) {
        return _Params(
          data: e,
          controller: TextEditingController(text: e.name),
        );
      }).toList();
    });
  }
}

Set initial value asynchronously in StateNotifier

StateNotifier<T> where T is the return data type. The variable defined in the super constructor is an initial value that is assigned to state variable. It assigns loading because _fetch function returns Future.

class _ParamsListNotifier extends StateNotifier<AsyncValue<List<_Params>>> {
  _ParamsListNotifier() : super(const AsyncValue.loading()) {
    _fetch();
  }

Named constructor

The following code is called a named constructor that provides different syntax constructor. A caller can use this function when it already has the data.

  _ParamsListNotifier.create(List<_Params> state)
      : super(AsyncValue.data(state));

Access to the AsyncValue state data

If you first look at this code you might not understand it. whenData function is one of functions defined in AsyncValue class provided by Riverpod. We set AsyncValue<List<_Params>> to T parameter. Therefore, the data type of state variable is also AsyncValue<List<_Params>>.

@override
void dispose() {
  state.whenData((value) {
    for (final data in value) {
      data.dispose();
    }
  });
  super.dispose();
}

We can not consume List<_Params> directly. whenData is triggered if the Future process defined in _fetch function has already been done. It is actually possible to access to List<_Params> by state.data.value but state.data can be null because a value is set to state variable asynchronously.

The state is probably not null in dispose function but it’s better to check if it’s not null.

Fetch data asynchronously

The last function tries to fetch data from the database. The process is async. Our StateNotifier class needs to return value even if the fetching process is running because AsyncValue provides the following states.

  • AsyncData
  • AsyncLoading
  • AsyncError

The first line in the function is for AsyncLoading.

Future<void> _fetch() async {
  state = const AsyncValue.loading();
  state = await AsyncValue.guard(() async {
    final data = await _fetchFromDatabase();
    return data.map((e) {
      return _Params(
        data: e,
        controller: TextEditingController(text: e.name),
      );
    }).toList();
  });
}

AsyncValue.guard() returns either AsyncData or AsyncError. The implementation looks like this.

static Future<AsyncValue<T>> guard<T>(Future<T> Function() future) async {
  try {
    return AsyncValue.data(await future());
  } catch (err, stack) {
    return AsyncValue.error(err, stack);
  }
}

If the process succeeds it returns AsyncValue.data, otherwise AsyncValue.error. For this reason, we don’t have to wrap our code with try-catch clause.

Try to implement with FutureProvider – NG Example

I tried to keep the structure. The first provider fetches data from the database and the second provider consumes it. I tried to use FutureProvider for the second provider but dispose function isn’t called.

Don’t use this code in your code. This is an improper way.

final _stateProvider = StateProvider((ref) => false);
Widget _createEditButton(BuildContext context) {
  return IconButton(
    onPressed: () => context.read(_stateProvider).state =
        !context.read(_stateProvider).state,
    icon: Icon(Icons.edit),
  );
}

final _provider1 = FutureProvider<List<MyData>>((ref) => _fetchFromDatabase());
final _futureProvider2 =
    FutureProvider.autoDispose<_ParamsListNotifier>((ref) async {
  print("_futureProvider2 was triggered");
  final value = await ref.watch(_provider1.future);
  final list = value.map((e) {
    return _Params(
      data: e,
      controller: TextEditingController(text: e.name),
    );
  }).toList();

  // Return our StateNotifier class
  return _ParamsListNotifier.create(list);
});

class _View2 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final button = _createEditButton(context);

    final listView = watch(_futureProvider2).maybeWhen(
      data: (data) {
        // Warning
        // The member 'state' can only be used within instance members of subclasses of
        // 'package:state_notifier/state_notifier.dart'
        return data.state.maybeWhen(
          data: (inState) {
            return ListView.builder(
              itemCount: inState.length,
              itemBuilder: (context, index) => TextField(
                controller: inState
                    .firstWhere((element) => element.data.uid == index)
                    .controller,
                enabled: watch(_stateProvider).state,
              ),
            );
          },
          orElse: () => Center(
            child: Text("Loading2"),
          ),
        );
      },
      orElse: () => Center(
        child: Text("Loading"),
      ),
    );

    return Scaffold(
      appBar: AppBar(title: Text("Multi Providers2")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
      persistentFooterButtons: [
        Visibility(
          child: Center(child: Text("FOOTER")),
          visible: context.read(_stateProvider).state,
        ),
      ],
    );
  }
}
final _stateProvider = StateProvider((ref) => false);
Widget _createEditButton(WidgetRef ref) {
  return IconButton(
    onPressed: () => ref.read(_stateProvider.state).state =
        !ref.read(_stateProvider.state).state,
    icon: Icon(Icons.edit),
  );
}

final _provider1 = FutureProvider<List<MyData>>((ref) => _fetchFromDatabase());
final _futureProvider2 =
    FutureProvider.autoDispose<_ParamsListNotifier>((ref) async {
  print("_futureProvider2 was triggered");
  final value = await ref.watch(_provider1.future);
  final list = value.map((e) {
    return _Params(
      data: e,
      controller: TextEditingController(text: e.name),
    );
  }).toList();

  // Return our StateNotifier class
  return _ParamsListNotifier.create(list);
});

class _View2 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final button = _createEditButton(ref);

    final listView = ref.watch(_futureProvider2).maybeWhen(
          data: (data) {
            return data.state.maybeWhen(
              data: (inState) {
                return ListView.builder(
                  itemCount: inState.length,
                  itemBuilder: (context, index) => TextField(
                    controller: inState
                        .firstWhere((element) => element.data.uid == index)
                        .controller,
                    enabled: ref.watch(_stateProvider.state).state,
                  ),
                );
              },
              orElse: () => Center(
                child: Text("Loading2"),
              ),
            );
          },
          orElse: () => Center(
            child: Text("Loading"),
          ),
        );

    return Scaffold(
      appBar: AppBar(title: Text("Multi Providers2")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
      persistentFooterButtons: [
        Visibility(
          child: Center(child: Text("FOOTER")),
          visible: ref.read(_stateProvider.state).state,
        ),
      ],
    );
  }
}

Since we use FutureProvider for _futureProvider2, maybeWhen function is used in the build function. It is used in the function as well because state of _futureProvider2 is also AsyncValue.

It looks working but the resource isn’t released. We defined dispose function as follows.

class _Params {
  ...

  void dispose() {
    controller.dispose();
    print("Data for uid ${data.uid} was disposed");
  }
}

It should write the log to the console but not in this example.

In addition to that, using data.state is not good outside of StateNotifier class.

// Warning
// The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'
return data.state.maybeWhen(

Provide StateNotifier with StateNotifierProvider

FutureProvider doesn’t call dispose of StateNotifier in the previous example. The right way to provide StateNotifier is to use StateNotifierProvider.

final _stateProvider = StateProvider((ref) => false);
Widget _createEditButton(BuildContext context) {
  return IconButton(
    onPressed: () => context.read(_stateProvider).state =
        !context.read(_stateProvider).state,
    icon: Icon(Icons.edit),
  );
}

final _provider2 = StateNotifierProvider.autoDispose<_ParamsListNotifier,
    AsyncValue<List<_Params>>>((ref) {
  print("_provider2 was triggered");
  return _ParamsListNotifier();
});

class _View1 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final button = _createEditButton(context);

    final listView = watch(_provider2).maybeWhen(
      data: (data) {
        return ListView.builder(
          itemCount: data.length,
          itemBuilder: (context, index) => TextField(
            controller: data
                .firstWhere((element) => element.data.uid == index)
                .controller,
            enabled: watch(_stateProvider).state,
          ),
        );
      },
      orElse: () => Center(
        child: Text("Loading"),
      ),
    );

    final footer = Row(
      children: [
        TextButton(
          onPressed: () async {
            context.read(_provider2).whenData((list) {
              for (final element in list) {
                element.controller.text = element.data.name;
              }
            });
          },
          child: const Text("Cancel"),
        ),
        TextButton(
          onPressed: () async {
            context.read(_provider2).whenData((list) async {
              String msg = "";
              for (final element in list) {
                msg += "${element.data.name} => ${element.controller.text}\n";
              }
              await showMessage(context, msg, "Result");
            });
          },
          child: const Text("OK"),
        ),
      ],
    );

    return Scaffold(
      appBar: AppBar(title: Text("Multi Providers")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
      persistentFooterButtons: [
        Visibility(
          child: footer,
          visible: context.read(_stateProvider).state,
        ),
      ],
    );
  }
}
final _stateProvider = StateProvider((ref) => false);
Widget _createEditButton(WidgetRef ref) {
  return IconButton(
    onPressed: () => ref.read(_stateProvider.state).state =
        !ref.read(_stateProvider.state).state,
    icon: Icon(Icons.edit),
  );
}

final _provider2 = StateNotifierProvider.autoDispose<_ParamsListNotifier,
    AsyncValue<List<_Params>>>((ref) {
  print("_provider2 was triggered");
  return _ParamsListNotifier();
});

class _View1 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final button = _createEditButton(ref);

    // final listView = ref.watch(_futureProvider).maybeWhen(
    final listView = ref.watch(_provider2).maybeWhen(
          data: (data) {
            return ListView.builder(
              itemCount: data.length,
              itemBuilder: (context, index) => TextField(
                controller: data
                    .firstWhere((element) => element.data.uid == index)
                    .controller,
                enabled: ref.watch(_stateProvider.state).state,
              ),
            );
          },
          orElse: () => Center(
            child: Text("Loading"),
          ),
        );

    final footer = Row(
      children: [
        TextButton(
          onPressed: () async {
            ref.read(_provider2).whenData((list) {
              for (final element in list) {
                element.controller.text = element.data.name;
              }
            });
          },
          child: const Text("Cancel"),
        ),
        TextButton(
          onPressed: () async {
            ref.read(_provider2).whenData((list) async {
              String msg = "";
              for (final element in list) {
                msg += "${element.data.name} => ${element.controller.text}\n";
              }
              await showMessage(context, msg, "Result");
            });
          },
          child: const Text("OK"),
        ),
      ],
    );

    return Scaffold(
      appBar: AppBar(title: Text("Multi Providers")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
      persistentFooterButtons: [
        Visibility(
          child: footer,
          visible: ref.read(_stateProvider.state).state,
        ),
      ],
    );
  }
}

The cancel button clears the updated text even after the OK button is pressed because the OK button doesn’t update the MyData class.

Both controller and MyData class are in the same class, We can easily get the controller and the corresponding data.

When the view is switched to another view, it writes the following output on the console. This is what we wanted to achieve.

I/flutter ( 6860): Data for uid 0 was disposed
I/flutter ( 6860): Data for uid 1 was disposed
I/flutter ( 6860): Data for uid 2 was disposed
I/flutter ( 6860): Data for uid 3 was disposed
I/flutter ( 6860): Data for uid 4 was disposed

Using .family provider

If there are two or more consumers that need the same data source, we need to have a provider that provides the data. We tried to do that once in the NG example above but it doesn’t call dispose function. Is there any solution?

We will create two providers. One is to provide a controller list. Another is to provide one of the controllers depending on a parameter. Let’s create a provider for a controller list.

final _dataProvider =
    FutureProvider<List<MyData>>((ref) => _fetchFromDatabase());
final _controllersProvider =
    StateProvider.autoDispose<Map<int, TextEditingController>>((ref) {
  return ref.watch(_dataProvider).maybeWhen(
        data: (list) {
          final Map<int, TextEditingController> controllers = {};
          for (final data in list) {
            controllers[data.uid] = TextEditingController(text: data.name);
          }
          return controllers;
        },
        orElse: () => {},
      );
});

As I mentioned above, we want to get a controller depending on a parameter. Therefore, Map is used to get the desired controller.

The next step is to create a provider that picks one of the controllers. Riverpod offers .family method for that.

final _controllerProvider =
    StateProvider.autoDispose.family<TextEditingController?, int>((ref, uid) {
  final controllers = ref.watch(_controllersProvider).state;
  ref.onDispose(()=>print("controller for uid $uid was disposed"));
  return controllers[uid];
});
final _controllerProvider =
    StateProvider.autoDispose.family<TextEditingController?, int>((ref, uid) {
  final controllers = ref.watch(_controllersProvider.state).state;
  ref.onDispose(() => print("controller for uid $uid was disposed"));
  return controllers[uid];
});

_controllersProvider always provides a state. It returns empty while fetch process is in progress. This controller provider returns null in this case. It returns null also if uid is not included in the list.

The Widget code looks like this.

class _View3 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final button = _createEditButton(context);

    final listView = watch(_dataProvider).maybeWhen(
      data: (list) {
        return ListView.builder(
          itemCount: list.length,
          itemBuilder: (context, index) {
            final myData = list[index];

            // pass a parameter to family provider
            final controller = watch(_controllerProvider(myData.uid)).state;
            return TextField(
              controller: controller,
              enabled: watch(_stateProvider).state,
            );
          },
        );
      },
      orElse: () => Center(
        child: Text("Loading3"),
      ),
    );

    return Scaffold(
      appBar: AppBar(title: Text("Multi Providers3")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
      persistentFooterButtons: [
        Visibility(
          child: Center(child: Text("FOOTER")),
          visible: context.read(_stateProvider).state,
        ),
      ],
    );
  }
}
class _View3 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final button = _createEditButton(ref);

    final listView = ref.watch(_dataProvider).maybeWhen(
          data: (list) {
            return ListView.builder(
              itemCount: list.length,
              itemBuilder: (context, index) {
                final myData = list[index];
                final controller =
                    ref.watch(_controllerProvider(myData.uid).state).state;
                return TextField(
                  controller: controller,
                  enabled: ref.watch(_stateProvider.state).state,
                );
              },
            );
          },
          orElse: () => Center(
            child: Text("Loading3"),
          ),
        );

    return Scaffold(
      appBar: AppBar(title: Text("Multi Providers3")),
      body: Column(
        children: [
          button,
          Expanded(child: listView),
        ],
      ),
      persistentFooterButtons: [
        Visibility(
          child: Center(child: Text("FOOTER")),
          visible: ref.read(_stateProvider.state).state,
        ),
      ],
    );
  }
}

I don’t show the video for this because it is the same as others but it should write the following messages to the console if you run the app.

I/flutter ( 4089): controller for uid 0 was disposed
I/flutter ( 4089): controller for uid 1 was disposed
I/flutter ( 4089): controller for uid 2 was disposed
I/flutter ( 4089): controller for uid 3 was disposed
I/flutter ( 4089): controller for uid 4 was disposed

End

How helpful is this article? I’m happy if this post solves your problem. Clone my repository if you want to check the complete code and try to run the example.

https://github.com/yuto-yuto/flutter_samples/blob/main/lib/riverpod/multi_providers.dart

Check the following posts as well that are related to this topic.

Comments

Copied title and URL