Flutter Riverpod Tutorial

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.

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()

Understand Providers

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

What is state then?

States actually refers 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.

There are many Providers in state management. There are six types of them and each of them have different usage. 

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.

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 notifer object. Once you 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 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 useage. 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()
        ),
      ),
    );
  }
}

Recent posts