Flutter Riverpod Tutorial

Created At: 2022-10-20 04:51:35 Updated At: 2023-09-09 20:55:46

Flutter Riverpod state management is an upgrade of Provider state management. It provides very convenient way to manage states and seperate the business logic from UI

In this article we will cover different kinds of Providers, how to use them and difference between and some interesting properties of Riverpod like ProviderScope, overrides and select property.

To install Riverpod run 

flutter pub get flutter_riverpod

After that the entry point would be your main() function of your app. You need to wrap your child inside ProviderScope . It's the entry point our Riverpod state management.

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

You see ProviderScope() holds our MyApp(). Because of this ProviderScope(), Riverpod Providers would be available in the widget tree. 

Understand Providers

In Riverpod when you create a state you need to do it using Providers.

What is state then?

States actually refer to special memory in the system where you data is stored. When we create states using Riverpod, it holds our data and manage the changes of the data and let the UI know about it.

Providers are the most important components of Riverpod. In short, you can think of providers as an access point to a shared state.

Creating Providers means creating shared state mechanism.

There are many Providers in state management. There are six types of them and each of them have different usage. There are two new Providers added in Riverpod 2.0

From the very simplest to the complex one.

  1. Provider
  2. StateProvider
  3. StateNotifierProvider
  4. FutureProvider
  5. StreamProvider
  6. ChangeNotifierProvider

The above six Providers, they all have different usage. Let's take a very quick look at the providers

A provider that creates read only value

final myValueProvider = Provider<MyValue>((ref) => MyValue());

A provider that expose a value which can be modified from outside

final myValueStateProvider = StateProvider<MyValue>((ref) => MyValue());

A provider that creates a StateNotifier and expose its current state.

inal myValueStateNotifierProvider = StateNotifierProvider((ref) => MyValueStateNotifier());

A provider that creates a stream and expose its latest event.

final myValueStreamProvider = StreamProvider<MyValue>((ref) => Stream.value(MyValue()));

A provider that asynchronously creates a single value.

final myValueFutureProvider = FutureProvider<MyValue>((ref) => Future.value(MyValue()));

Provider & StateProvider

Look how to use Provider and StateProvider in the below example.

In the example Provider and StateProvider work together to work on boolean values and string values. 

The very basic Provider can not be changed from ConsumerWidget, we can only read it. But StateProvider could be changed from ConsumerWidget.

StateProvider deals with basic data types like integer, string and boolean while Provider can deal with any types of data.

final selectedButtonProvider = StateProvider<String>((ref) => '');

For now, we just returned an empty String from StateProvider. Later we will see how change the String value.

selectedButtonProvider is our new provider and we will change the data value by invoking notifier object. Once you use Riverpod for state management, notifier object and state object would be available.

Inside the ElevatedButton() we used this property to toggle the string values.

          ElevatedButton(
            onPressed: () => ref.read(selectedButtonProvider.notifier).state = 'red',
            child: Text('Red'),
          ),

This notifier object is not available for basic Provider. The other elevated button has done the same thing.

Here we used ref.read() to access our Provider and change it's value. In general we will use ref.read() for changing data, and ref.watch() for reading data.

import 'package:flutter_riverpod/flutter_riverpod.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return  MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text("Riverpod App"),
          ),
          body: SelectedButton(),
        ),
      );
  }
}

final isRedProvider = Provider<bool>((ref){
  final color = ref.watch(selectedButtonProvider);
  return color == 'red'; // true if red
});
final selectedButtonProvider = StateProvider<String>((ref) => '');

class SelectedButton extends ConsumerWidget {
  const SelectedButton({ Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isRed = ref.watch(isRedProvider);
    final selectedButton = ref.watch(selectedButtonProvider);
    return Center(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(selectedButton),
          ElevatedButton(
            onPressed: () => ref.read(selectedButtonProvider.notifier).state = 'red',
            child: Text('Red'),
          ),
          ElevatedButton(
            // 5
            onPressed: () => ref.read(selectedButtonProvider.notifier).state = 'blue',
            child: Text('Blue'),
          ),
          isRed ? Text('Color is red') : Text('Color is blue')
        ],
      ),
    );
  }
}

StateNotifierProvider

This provider is used to expose the value held by StateNotifier. With StateNotifier you can deal with any kinds of data type.

So if your data type is complex like List, Maps or custom objects, then StateNotifier is the way to go. Your data would be held inside StateNotifier and they would be exposed to the outside world, I mean to the ConsumerWidgets by StateNotifierProvider.

Here we used StateNotifier and StateNotifierProvider to do CRUD operations. Inside StateNotifier we use List<String> type data.

StateNotifier must be extended by another class, In the super class constructor there should be initialization of data.

class NumberNotifier extends StateNotifier<List<String>> {
  NumberNotifier() : super(['number 12', 'number 30']);
}

We created a class name NumberNotifier which extends StateNotifier. Let's see how exposed the shared data to ConsumerWidget.

final numbersProvider =
StateNotifierProvider<NumberNotifier, List<String>>((ref) {
  return NumberNotifier();
});

Now using numbersProvider, we would be able to access the data List of Strings from UI. 

We can use NumberNotifier() class as a normal class and add different kinds of methods for our usage. Later we will see that we added CRUD methods to it.

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

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

class MyApp extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}

final numbersProvider =
StateNotifierProvider<NumberNotifier, List<String>>((ref) {
  return NumberNotifier();
});

class NumberNotifier extends StateNotifier<List<String>> {
  NumberNotifier() : super(['number 12', 'number 30']);

  void add(String number) {
    state = [...state, number];
  }


  void remove(String number) {
    state = [...state.where((element) => element != number)];
  }

  void update(String number, String updatedNumber) {
    final updatedList = <String>[];
    for (var i = 0; i < state.length; i++) {
      if (state[i] == number) {
        updatedList.add(updatedNumber);
      } else {
        updatedList.add(state[i]);
      }
    }
    state = updatedList;
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final numbers = ref.watch(numbersProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Riverpod')),

      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref
              .read(numbersProvider.notifier)
              .add('number ${Random().nextInt(100)}');
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        child: Column(
          children: numbers.map((e) => GestureDetector(
            onLongPress: () {
              ref
                  .read(numbersProvider.notifier)
                  .update(e, '${e } '+Random().nextInt(1000).toString());
            },
            onTap: () {
              ref.read(numbersProvider.notifier).remove(e);
            },

            child: Padding(
              padding: EdgeInsets.all(10),
              child: Text(e),
            ),
          )).toList()
        ),
      ),
    );
  }
}

Fromt the above code, you see that, we can also do custom operation on List if the list is stateful. In our the List is stateful.

Because we have used StateNotifier<T> type List<String>, this is way, the List of Strings is stateful.

Riverpod overrides

Here we will see what is overrides property of Riverpod. As the name suggest this property overrides something. What does it override?

It overrides a value of Provider (Riverpod provider), it overrides the previous value. This property is specially useful if you want to change the value of Provider, out of it's scope. Provider provides or gives a read only value which means you can not change Provider's value out of it. You can only change the value inside Provider.

final indexProvider = Provider<int>((_) {
  return 0;
});

indexProvider's value could be only changed inside the curly braces. But if you really want to change the value out of it, then we can use ProviderScope and overrides property.

return  ProviderScope(
              overrides: [indexProvider.overrideWith((ref) => index)],
              child: .......
          )

With this indexProvier gets a new value. You may use the ProviderScope in a loop as well, in that indexProvider would get a new value during each iteration based on the loop index.

More about ProviderScope and overrides here

Update a List

Here we will see how to udate a list. Your list could be of any type from int, string to custom object. Regardless what's inside the list, the update method is same. You may use the below lines code to update a list

final list = ref.read(listProvider);
list[index]=Faker().person.firstName(); //here you can use any object type like int, string or custom object
ref.watch(listProvider.notifier).state=[...list];

All you need to do, get a new value for a certain index and then use notifier and state object from your provider and after that use spread operator with your list.

Let's see the core syntax.

ref.watch(listProvider.notifier).state=[...list];

Riverpod select()

Riverpod select is particularly use if you want stop rebuild of your widget when you don't need. In general if you list, and you update one of the list items, the whole list gets rebuild, not just a certain item.

To prevent the whole list from being build, we can use select() function and with this function, you can mention for what property change the whole list should get rebuild. Let's take a look

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ListView.builder(
        itemCount: ref.watch(listProvider.select((value) => value.length)),
        itemBuilder: (_, index){
          print("...list view index $index...");
          return  ProviderScope(
              overrides: [indexProvider.overrideWith((ref) => index)],
              child: const _ListItem()
          );
        }
    );
  }

Here in the itemCount property we have used select() function. By this here we are saying, only rebuild the whole list, if length of the list is changed. 

If your length of the list does not change, your list would not get rebuild and this is performance optimized.

Comment

Add Reviews