What Is Riverpod? Here’s What I Learned from Different Resources

 

Hello everyone.

I’ve been obsessed with StateManagement in Flutter lately, because as my knowledge of Flutter increases, I’m attempting to make more comprehensive projects. However, I always get stuck when it comes to moving, storing and retrieving data. I’ve tried to use the provider package a bit before, but provider is not enough for the application I’m working on right now. So when my research pointed out that Riverpod can be used in more extensive projects than provider, I started working on it.

And now I’m here to compile the information I got first from the documentation and then from different sources. I hope I will inspire you. Let’s get started.

What is Riverpod?

I hope my first note about Riverpod will be a kind of icebreaker. Riverpod is an anagram of provider, a different arrangement of the same letters. :)

Let’s see what it is. Riverpod is a modern and flexible state management solution in Flutter. It’s more secure, independent and testable than traditional approaches, and with it we can manage state in our application more effectively.

So why do we need StateManagement? I would like to talk a little bit about this before going into the details of Riverpod.

Why State Management?

Imagine we are building an application with many different screens and widgets. And some of them may need to access the same state, such as the productState. For example, in the tree diagram below, the Home Screen and the Shopping Cart widget may both need the same productState.

Once a state is defined in the Home Screen, to update it we need to create a function or state within that widget and then pass that function or state up to the shopping cart widget. When we do this, the widget tree is rebuilt to reflect this state change. This is a relatively simple and common scenario up to a point. But another common scenario is having to use the operation in more than one place, like this one, where we define the state at the vertex of the tree and then pass it through the widgets to other places where it is needed. When the latter scenario becomes more common in practice, it can lead to a bit messy and hard to maintain code, I know because it happened to me :). This is where a state management solution simplifies things for us.

As I show in the diagram below, once we define the function or state with the provider at the top, we can call it wherever we want without going through the entire hierarchy in the widget tree.

Now that we have talked about what riverpod is and why State Management is necessary, we can start using riverpod.

Installation

First of all, as in every package usage, we install our package in our project.

flutter pub add flutter_riverpod

And then it is added to the pubspec.yaml file with whatever the current version is as follows.

dependencies:
flutter_riverpod: ^2.5.3

We import it to the page we will use as follows.

import 'package:flutter_riverpod/flutter_riverpod.dart';

Usage

We wrap the runApp function in the main method of our project with ProviderScope. This is used to keep the states of all created providers.

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

By the way, provider in software literally means an object that covers a state and allows this state to be listened to. And it means the same thing in all StateManagement solutions and is simply used with the same logic.

At this point I want to write a simple example of a provider object. Like every function or property definition, provider also has a return type. In this example, this provider returns a String.

final helloRiverpodProvider = Provider<String>((ref) {
return 'Hello Riverpod';
});

We write this provider object as a global variable outside all methods and widgets in order to be able to access it from anywhere on the page, in all widgets we will write. Now let’s see how to use this global variable.

Using Provider Object in Widget

1- With ConsumerWidget

class HelloRiverpodWidget extends ConsumerWidget { 
const HelloRiverpodWidget({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
return Container();
}
}

The widget we create here is extended from Consumer Widget, not from Stateless or Stateful Widget. ConsumerWidget works like Stateless Widget. In the build method, the WidgetRef parameter must be present as well as the BuildContext parameter.

We read the variable defined as a global variable with the ref.watch method and use it by assigning it to a separate variable in the widget.

class HelloRiverpodWidget extends ConsumerWidget {
const HelloRiverpodWidget({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final helloRiverpod = ref.watch(helloRiverpodProvider);
return Text(helloRiverpod);
}
}

2- With Consumer

When we wrap the whole Scaffold with ConsumerWidget, the whole Scaffold is refreshed every time there is a change in the Provider object. This can mean extra performance issues. Instead we can wrap only the Widget containing the Provider object with Consumer.

class HelloRiverpodWidget extends StatelessWidget {
const HelloRiverpodWidget({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer(
builder: (context, ref, child) {
final helloRiverpod = ref.watch(helloRiverpodProvider);
return Text(helloRiverpod);
},
),
);
}
}

Here we need to configure the builder well, which is required when we create a consumer. For this reason, it is more detailed than ConsumerWidget.

Instead, if we configure our widgets to be reusable and break them into small pieces, we can continue to use ConsumerWidget and minimize the performance problem related to rebuild.

3- With ConsumerStatefulWidget

I mentioned that ConsumerWidget is the counterpart of StatelessWidget. In cases where we need to use StatefulWidget, we can use ConsumerStatefulWidget as the counterpart.

Here we have two methods to call the provider object. The first is to call it in initState().

@override
void initState() {
super.initState();
final helloRiverpod = ref.read(helloRiverpodProvider);
print(helloRiverpod);
}

The other method is to call it in the classic widget tree.

class HelloRiverpodStful extends ConsumerStatefulWidget {
const HelloRiverpodStful({super.key});

@override
ConsumerState<ConsumerStatefulWidget> createState() => _HelloRiverpodStfulState();
}

class _HelloRiverpodStfulState extends ConsumerState<HelloRiverpodStful> {

@override
Widget build(BuildContext context) {
final helloRiverpod = ref.watch(helloRiverpodProvider);
return Text(helloRiverpod);
}
}

If we use the WidgetRef ref in the build method, we call it with watch(), if we use it in one of the other methods, we call it with read(). Both cases return a value. I will compare these two methods in more detail below.

WidgetRef ref is evaluated as an argument in Consumer or ConsumerWidget and as a property in ConsumerState.

WidgetRef is the object that allows widgets like BuildContext to interact with provider objects. BuildContext allows to access ancestor widgets in the widget tree. Like Theme.of(context) or MediaQuery.of(context). WidgetRef allows accessing any provider in the application. Because all Riverpod providers are global.

ref.watch() vs ref.read()

In the build method, we use ref.watch() to observe the state of a provider object and rebuild it if it changes.

To read the state of a provider object only once (like initState) we use ref.read().

In the onPressed callback method of a button we use ref.read(), not ref.watch.

Sometimes we want to show SnackBar or alertDialog when a provider state changes. We can do this by calling the ref.listen() method inside the build method.

ref.listen() provides a callback when the provider object changes, not when build is called. So it can also be used to run asynchronous code.

In my article, I shared my Riverpod notes that I researched and learned from different sources. Before I conclude, I would like to mention a plugin for vscode: Flutter Riverpod Snippets.

This plugin speeds up writing code by automatically completing frequently used Riverpod code snippets. For example, you can quickly add basic code for common Riverpod constructs like Provider, StateNotifier, ConsumerWidget, etc. so you don’t have to write from scratch every time. It also helps you minimize bugs.

I continue to learn about Riverpod, because I can see that it is a vast subject, like the general software world description. I will write about what I learn in a second article in the future.

Error-free code to all of us.

Thank you for reading.

I would appreciate if you subscribe to be informed about my new articles.

Selin.

Hiç yorum yok: