Re-walking the road of Flutter state management—Introduction to Riverpod

Re-walking the road of Flutter state management—Introduction to Riverpod

Friends who are familiar with me should know that I wrote a series of “The Road to Flutter State Management” several years ago. At that time, I introduced Provider, which is also an officially recommended state management tool, but I didn’t finish it at that time, because I wrote it. I feel that there are many places that are not satisfactory, and it is very awkward to use, so after writing 7 articles, I put it on hold for the time being.

After so long, there is still no state management framework in Flutter that can crush everything. GetX may be, but I don’t think it is. The state management of the InheritedWidget system should be the orthodox state management.

Recently, when I was paying attention to the follow-up progress of Provider, I accidentally discovered a new library-Riverpod, which is known as a new generation of state management tools. Take a closer look, hey, he is still the author of Provider. your own feet.

As the author said, Riverpod is a rewrite of Provider, isn’t it? The letters have not changed, but the order has been changed. The name is also extensive and profound.

In fact, Provider is already very good in use, but with the deepening of Flutter, everyone’s demand for it is getting higher and higher, especially for the exception caused by the InheritedWidget level problem in the Provider and the use of BuildContext. Many, and Riverpod, on the basis of Provider, has explored a path of heart state management.

You can take a look at the official document https://riverpod.dev first. After reading it, you will find that you are still confused. That’s right. Like Provider, Riverpod has many types of Providers, which are used in different scenarios. Therefore, clarifying the different roles and usage scenarios of these Providers is very helpful for us to make good use of Riverpod.

Although the documentation on the official website is carefully written by the author, its tutorials are from the perspective of a creator, so many beginners seem to be a little confused about the direction, so this is the reason for this series. article.

In this series, I will lead you to conduct an intensive reading and appreciation of the documents. This article is not all translation of the documents, and the order of explanation is different. Therefore, if you want to get started with Riverpod for state management, then this article must be Your best choice.

Provider first look

First of all, why do we need to carry out state management? State management is to solve the declarative UI development. It is a processing operation about data state. For example, Widget A depends on the data of Widget B of the same level. At this time, we can only put the data state on the data. Mention their parent classes, but this is more troublesome. State management frameworks such as Riverpod and Provider are created to solve similar problems.

Wrapping a state in a Provider can have the following benefits.

  • Allows easy access to the state in multiple locations. Providers can completely replace patterns such as Singletons, Service Locators, Dependency Injection or InheritedWidgets
  • Simplifying the combination of this state and other states, have you ever been troubled by how to combine multiple objects into one? This scenario can be implemented directly inside the Provider
  • Implemented performance optimization. Whether filtering Widget rebuilds, or caching expensive state computations; Provider ensures that only parts affected by state changes are recomputed
  • Increases the testability of your application. With Provider, you don’t need complicated setUp/tearDown steps. Also, any Provider can be overridden to have different behavior during testing, which makes it easy to test a very specific behavior
  • Allows easy integration with advanced features like logging or pull-to-refresh

First, let’s use a simple example to get a feel for how Riverpod manages state.

Providers are the most important part of a Riverpod application. Provider is an object that encapsulates a state and allows listening to that state. Providers come in many variants, but they all work the same way.

The most common usage is to declare them as global constants, such as the following.

 final myProvider = Provider((ref) { return MyValue(); });

Don’t be intimidated by Provider’s global variables. Provider is completely final. Declaring a Provider is no different than declaring a function, and Providers are testable and maintainable.

This code consists of three parts.

  • final myProvider, a variable declaration. This variable is what we will use in the future to read the state of our provider. Provider should always be final
  • Provider, the type of provider we decide to use. Provider is the most basic of all Provider types. It exposes an object that never changes. We can replace Providers with other Providers such as StreamProvider or StateNotifierProvider to change the way the values ​​interact
  • A function that creates shared state. The function will always receive an object named ref as a parameter. This object allows us to read other providers, perform some actions when the state of our provider will be destroyed, and a few other things

The type of object returned by the function passed to the provider depends on the provider used. For example, a Provider function can create any object. On the other hand, the StreamProvider’s callback will be expected to return a Stream.

You can declare as many Providers as you want without restriction. Instead of using package:provider, Riverpod allows you to create multiple providers that expose the same “type” of state.

 final cityProvider = Provider((ref) => 'London'); final countryProvider = Provider((ref) => 'England');

Both providers create a string, but that doesn’t have any problems.

In order for Provider to work, you must add a ProviderScope at the root of your Flutter app.

 void main() { runApp(ProviderScope(child: MyApp())); }

The above is the simplest use of Riverpod. Let’s take a look at the complete sample code.

 import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // We create a "provider", which will store a value (here "Hello world"). // By using a provider, this allows us to mock/override the value exposed. final helloWorldProvider = Provider((_) => 'Hello world'); void main() { runApp( // For widgets to be able to read providers, we need to wrap the entire // application in a "ProviderScope" widget. // This is where the state of our providers will be stored. ProviderScope( child: MyApp(), ), ); } // Extend ConsumerWidget instead of StatelessWidget, which is exposed by Riverpod class MyApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final String value = ref.watch(helloWorldProvider); return MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('Example')), body: Center( child: Text(value), ), ), ); } }

It can be found that the use of Riverpod is simpler than package:Provider. It declares a global variable to manage state data, and then can obtain data anywhere.

How to read the status value of Provider

After having a simple understanding, let’s first understand the “reading” in the state.

In Riverpod, instead of relying on the BuildContext like package:Provider, we have a “ref” variable instead. This thing is the link between the two sides of the accessor. This object allows us to interact with the Provider, whether it is from a Widget or another Provider.

Get ref from Provider

All providers have a “ref” as a parameter.

 final provider = Provider((ref) { // use ref to obtain other providers final repository = ref.watch(repositoryProvider); return SomeValue(repository); })

This parameter can be safely passed to other providers or classes to get the desired value.

For example, a common use case is to pass a Provider’s “ref” to a StateNotifier.

 final counterProvider = StateNotifierProvider<Counter, int>((ref) { return Counter(ref); }); class Counter extends StateNotifier<int> { Counter(this.ref): super(0); final Ref ref; void increment() { // Counter can use the "ref" to read other providers final repository = ref.read(repositoryProvider); repository.post('...'); } }

Doing this enables our Counter class to read the Provider.

This way is an important way to connect components and providers.

Get ref from Widget

Widgets naturally don’t have a ref parameter. But Riverpod provides various solutions to get this parameter from widget.

Extend ConsumerWidget

The most common way to get a ref in the widget tree is to use ConsumerWidget instead of StatelessWidget.

ConsumerWidget is the same as StatelessWidget in usage, the only difference is that its construction method has an extra parameter: “ref” object.

A typical ConsumerWidget looks like this.

 class HomeView extends ConsumerWidget { const HomeView({Key? key}): super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { // use ref to listen to a provider final counter = ref.watch(counterProvider); return Text('$counter'); } }
Extending ConsumerStatefulWidget

Similar to ConsumerWidget, ConsumerStatefulWidget and ConsumerState are equivalent to a StatefulWidget with state, the difference is that state has a “ref” object.

This time, “ref” is not passed as an argument to the build method, but as a property of the ConsumerState object.

 class HomeView extends ConsumerStatefulWidget { const HomeView({Key? key}): super(key: key); @override HomeViewState createState() => HomeViewState(); } class HomeViewState extends ConsumerState<HomeView> { @override void initState() { super.initState(); // "ref" can be used in all life-cycles of a StatefulWidget. ref.read(counterProvider); } @override Widget build(BuildContext context) { // We can also use "ref" to listen to a provider inside the build method final counter = ref.watch(counterProvider); return Text('$counter'); } }

Get state by ref

Now that we have a “ref”, we can start using it.

ref “has three main uses.

  • Get the value of a Provider and listen for changes, so that when the value changes, this will rebuild the Widget or Provider that subscribed to the value. This is done via ref.watch
  • Add a listener on a Provider to perform an action, such as navigating to a new page or performing some action when the Provider changes. This is done via ref.listen
  • Get the value of a Provider while ignoring its changes. This is useful when we need a Provider’s value in an event, such as a “click action”. This is done via ref.read

Whenever possible, it is best to use ref.watch instead of ref.read or ref.listen for a function.
By relying on ref.watch, your application becomes both reactive and declarative, which makes it easier to maintain.

Observe the state of the Provider through ref.watch

ref.watch is used in the Widget’s construction method, or in the body of the Provider, so that the Widget/Provider can listen to another Provider.

For example, Providers can use ref.watch to combine multiple Providers into a new value.

An example is to filter a todo-list, we need two providers.

  • filterTypeProvider, a Provider that exposes the current filter type (None, meaning only completed tasks are displayed)
  • todosProvider, a provider that exposes the entire todo list

By using ref.watch, we can make a third provider that combines the two to create a filtered task list.

 final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none); final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList()); final filteredTodoListProvider = Provider((ref) { // obtains both the filter and the list of todos final FilterType filter = ref.watch(filterTypeProvider); final List<Todo> todos = ref.watch(todosProvider); switch (filter) { case FilterType.completed: // return the completed list of todos return todos.where((todo) => todo.isCompleted).toList(); case FilterType.none: // returns the unfiltered list of todos return todos; } });

With this code, filteredTodoListProvider can now manage the filtered todo list.

If the filter or task list changes, the filtered list is also automatically updated. Also, if neither the filter nor the task list has changed, the filtered list will not be recalculated.

Similarly, a Widget can use ref.watch to display content from the Provider and update the user interface when that content changes.

 final counterProvider = StateProvider((ref) => 0); class HomeView extends ConsumerWidget { const HomeView({Key? key}): super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { // use ref to listen to a provider final counter = ref.watch(counterProvider); return Text('$counter'); } }

This code shows a Widget listening to a provider that stores counts. If the count changes, the widget will rebuild and the UI will update to show the new value.

The ref.watch method should not be called asynchronously, such as in onPressed of ElevatedButton. Nor should it be used during the lifetime of initState and other States. In these cases, consider using ref.read instead.

Listen for changes in Provider through ref.listen

Similar to ref.watch, you can use ref.listen to watch a Provider.

The main difference between them is that if the monitored provider changes, using ref.listen will not rebuild the widget/provider, but will call a custom function.

This is useful for doing something when a change occurs, such as showing a snackbar when an error occurs.

The ref.listen method takes 2 parameters, the first is the Provider and the second is the callback function we want to execute when the state changes. The callback function will be passed 2 values ​​when called, the value of the previous state and the value of the new state.

The ref.listen method can also be used in the body of the Provider.

 final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref)); final anotherProvider = Provider((ref) { ref.listen<int>(counterProvider, (int? previousCount, int newCount) { print('The counter changed $newCount'); }); // ... });

Or use it in a Widget’s Build method.

 final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref)); class HomeView extends ConsumerWidget { const HomeView({Key? key}): super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { ref.listen<int>(counterProvider, (int? previousCount, int newCount) { print('The counter changed $newCount'); }); return Container(); } }

ref.listen should also not be called asynchronously, like in onPressed of ElevatedButton. Nor should it be used during the lifetime of initState and other States.

Read the state of the Provider through ref.read

The ref.read method is a way to get the state of the provider without listening.

It is typically used in functions triggered by user interaction. For example, we can use ref.read to increment a counter when the user clicks a button.

 final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref)); class HomeView extends ConsumerWidget { const HomeView({Key? key}): super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { // Call `increment()` on the `Counter` class ref.read(counterProvider.notifier).increment(); }, ), ); } }

ref.read should be avoided as much as possible as it is not reactive.

It exists in situations where using watch or listen would cause problems. It’s almost always better to use watch/listen if you can, especially watch.

About when to use ref.read

First, never use ref.read directly in a widget’s build function.

You may be tempted to use ref.read to optimize the performance of a Widget, for example with the following code.

 final counterProvider = StateProvider((ref) => 0); Widget build(BuildContext context, WidgetRef ref) { // use "read" to ignore updates on a provider final counter = ref.read(counterProvider.notifier); return ElevatedButton( onPressed: () => counter.state++, child: const Text('button'), ); }

But this is a very bad practice and leads to hard-to-trace bugs.

Using ref.read in this way is often associated with the idea: “The value exposed by the Provider will never change, so it is safe to use ‘ref.read'”. The problem with this assumption is that while the Provider may indeed never update its value today, there is no guarantee that it will do so tomorrow.

Software tends to change a lot, and it is likely that in the future, a value that has never changed before needs to be changed.

If you use ref.read, when the value needs to change, you’ll have to go through the entire codebase and change ref.read to ref.watch – it’s error-prone, and you’re likely to forget some cases.

If you use ref.watch in the first place, you will have fewer problems when refactoring.

But what if I want to use ref.read to reduce the number of times my widget is refactored?

While this goal is laudable, it’s important to note that you can use ref.watch instead to achieve the exact same effect (less builds).

Provider provides various methods to get a value while reducing the number of rebuilds, you can use these methods instead.

For example the code below (bad).

 final counterProvider = StateProvider((ref) => 0); Widget build(BuildContext context, WidgetRef ref) { StateController<int> counter = ref.read(counterProvider.notifier); return ElevatedButton( onPressed: () => counter.state++, child: const Text('button'), ); }

We can change it this way.

 final counterProvider = StateProvider((ref) => 0); Widget build(BuildContext context, WidgetRef ref) { StateController<int> counter = ref.watch(counterProvider.notifier); return ElevatedButton( onPressed: () => counter.state++, child: const Text('button'), ); }

Both snippets achieve the same effect: our button will not rebuild when the counter is incremented.

On the other hand, the second method supports the case where the counter is reset. For example, another part of the application can call.

 ref.refresh(counterProvider);

This will recreate the StateController object.

If we use ref.read here, our button will still use the previous instance of StateController, which has been deprecated and should not be used anymore.

While using ref.watch the button is correctly rebuilt, using the new StateController.

What values ​​can be read by ref.read

Depending on the provider you want to listen to, you may have multiple possible values ​​to listen to.

As an example, consider the StreamProvider below.

 final userProvider = StreamProvider<User>(...);

When reading this userProvider you can do like below.

  • Synchronously read the current state by listening to the userProvider itself.
 Widget build(BuildContext context, WidgetRef ref) { AsyncValue<User> user = ref.watch(userProvider); return user.when( loading: () => const CircularProgressIndicator(), error: (error, stack) => const Text('Oops'), data: (user) => Text(user.name), ); }
  • Obtain the relevant Stream by listening to userProvider.stream.
 Widget build(BuildContext context, WidgetRef ref) { Stream<User> user = ref.watch(userProvider.stream); }
  • Obtain a Future by listening to userProvider.future, which resolves with the latest issued value.
 Widget build(BuildContext context, WidgetRef ref) { Future<User> user = ref.watch(userProvider.future); }

Other providers may provide different alternative values.

For more information, see the API reference, refer to each provider’s API documentation.

Control the precise read range with select

The last feature to mention about reading Providers is the ability to reduce the number of times Widget/Provider rebuilds from ref.watch, or how often ref.listen executes functions.

This is important because by default, listening to a Provider will listen to the state of the entire object. But sometimes, a Widget/Provider may only care about some property changes, not the whole object.

For example, a Provider might expose a User object.

 abstract class User { String get name; int get age; }

But a Widget may only use the username.

 Widget build(BuildContext context, WidgetRef ref) { User user = ref.watch(userProvider); return Text(user.name); }

If we simply use ref.watch, this will rebuild the widget when the user’s age changes.

The solution is to use select to explicitly tell Riverpod that we only want to listen to the user’s first name property.

The updated code will be like this.

 Widget build(BuildContext context, WidgetRef ref) { String name = ref.watch(userProvider.select((user) => user.name)); return Text(name); }

By using select, we can specify a function to return the property we care about.

Whenever the user changes, Riverpod will call this function and compare the previous and new results. If they are different (eg when the name changes), Riverpod will rebuild the Widget. However, if they are equal (such as when the age changes), Riverpod will not rebuild the Widget.

This scenario can also use select and ref.listen.

 ref.listen<String>( userProvider.select((user) => user.name), (String? previousName, String newName) { print('The user name changed $newName'); } );

Doing this will also only call the listener when the name changes.

Also, you don’t necessarily have to return a property of the object. Any value that overrides == will work. For example, you can do this.

 final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));

Reading the status is a very important part. When and how to read it, it will have different effects.

ProviderObserver

ProviderObserver can listen for changes in a ProviderContainer.

To use it, you can extend the ProviderObserver class and override the methods you want to use. ProviderObserver has three methods.

  • didAddProvider: called every time a Provider is initialized
  • didDisposeProvider: called every time the Provider is destroyed
  • didUpdateProvider: will be called every time the Provider is updated

A simple use case for ProviderObserver is to record changes to the provider by overriding the didUpdateProvider method.

 // A Counter example implemented with riverpod with Logger class Logger extends ProviderObserver { @override void didUpdateProvider( ProviderBase provider, Object? previousValue, Object? newValue, ProviderContainer container, ) { print(''' { "provider": "${provider.name ?? provider.runtimeType}", "newValue": "$newValue" }'''); } } void main() { runApp( // Adding ProviderScope enables Riverpod for the entire project // Adding our Logger to the list of observers ProviderScope(observers: [Logger()], child: const MyApp()), ); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp(home: Home()); } } final counterProvider = StateProvider((ref) => 0, name: 'counter'); class Home extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider); return Scaffold( appBar: AppBar(title: const Text('Counter example')), body: Center( child: Text('$count'), ), floatingActionButton: FloatingActionButton( onPressed: () => ref.read(counterProvider.notifier).state++, child: const Icon(Icons.add), ), ); } }

Now, whenever our Provider’s value is updated, the logger will log it.

 I/flutter (16783): { I/flutter (16783): "provider": "counter", I/flutter (16783): "newValue": "1" I/flutter (16783): }

For changeable states like StateController (state of StateProvider.state) and ChangeNotifier, previousValue and newValue will be the same. Because they are referencing the same StateController/ChangeNotifier.

These are the most basic understanding of Riverpod, but it is a very important part, especially how to read the state value, which is the core of our good use of Riverpod.

Recommend my website to everyone https://xuyisheng.top/Focus on Android-Kotlin-Flutter Welcome everyone to visit

Re-walking the road of Flutter state management—Introduction to Riverpod

This article is reprinted from: https://xuyisheng.top/riverpod1/
This site is for inclusion only, and the copyright belongs to the original author.

Leave a Comment