Explaining Riverpod Concepts to a 5 y/o

Explaining Riverpod Concepts to a 5 y/o

ยท

10 min read

Hello there ๐Ÿ‘‹๐Ÿฝ, I got motivated to write this article because I started out the year with a 30 days challenge to learn something new related to my tech stack (Flutter). So I would be explaining some concepts and how I understood them via various resources I leveraged online
I choose to learn Riverpod because I was quite familiar with PROVIDER and also wanted to level up since they were some limitations were using the provider package, And Riverpod being an anagram of PROVIDER solved some critical issues and makes managing state easier and more fluid.
Moving on I would be breaking down some technical terms associated with Riverpod into simple terms to ease understanding but it would be up to you to play around with these concepts to fully understand it.
First off Riverpod can be best kept as a state management solution although it is technically said to be a "Reactive Caching and Data-binding Framework" don't get any of these twisted...(at least not yet) ๐Ÿ˜… it is simply used to manage state (moving data around to different location the data is needed in our App).
Riverpod as I said earlier is more than just an anagram of PROVIDER but a vast improvement of the provider package itself, Riverpod works by using different types of "PROVIDERS" to supply & listen to data in our depending on the use case.

Types of PROVIDERS in RiverPod :

  • PROVIDER: A provider used to provide a set of data or values (of any type) that would be read generally in the App. It is a READ-ONLY type of Provider in Riverpod.

  • STATE PROVIDER: It's a provider that is used to provide a value(of any type) that can or would be changed, varied or modified within the App.

  • FUTURE PROVIDER: It's a provider that is used to provide a value after resolving or waiting for a future value or function.

  • STREAM PROVIDER: It's a provider that is used to provide a value after resolving or waiting for a stream value or function.

  • CHANGE NOTIFIER PROVIDER: This type of provider works alongside with Change Notifier which comes with Flutter, and it is used to provide and modify mutable (changeable) states in our App.

  • STATE NOTIFIER PROVIDER: This type works alongside with State Notifier and it helps us to assign data to the state directly which is helpful as it would prevent us from making deliberate or accidental changes to the data within the state, It is the most recommended one to use.

The concept behind Riverpod Providers...๐Ÿค”

One thing to note is before any of these providers are used we must wrap our entire App in a PROVIDER SCOPE, which essentially allows our app to see and use any of our created providers

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

The above-listed providers' supply and read data within our app based on a keyword called "ref" which can be said to be a reference by which our provider can be used in our app

What is ref: it is how you access or read a provider to get data from the provider

Also, note how we can get access to ref, A convenient way to access ref from anywhere in our app by wrapping our app with the ConsumerWidget

final valueStateProvider = StateProvider.autoDispose<int>(((ref) => 50));

//Extending ConsumerWidget to access "ref"
class StateProviderPage extends ConsumerWidget {
  const StateProviderPage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
     //returns the value from the provider and rebuild if the value changes
    final value = ref.watch(valueStateProvider);

    return Scaffold(
        ...

    );
  }
}

So based on any provider you choose to use, this concept above is fundamental and necessary, I would be leaving a helpful Github Repo below where I properly documented the codebase so you can easily read through the code comments, run the app and understand the inner working of Riverpod and it's usage but in the meantime, I'll explain a few demo examples ๐Ÿ› 

I would explain how ChangeNotifier is used although it might be an easier route to quickly perform CRUD (Create, Read, Update, Delete) operations and mutate values, It isn't recommended to use according to the Riverpod docs, because it kind of creates vulnerability by exposing your data leaving them mutable hence StateNotifer is more recommended!

From the code snippet below whose purpose is to perform CRUD operation on the List (_pile)...First would be to specify that we want the class to be a ChangeNotifer, afterward, we move on to the explanation of the logic which has been adequately commented below

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

//LAST STEP; We create ChangeNotifierProvider to pass StockPileNotifier to //our App
final stockPileChangeNotifierProvider =
    ChangeNotifierProvider<StockPileNotifier>((ref) {
  return StockPileNotifier();
});

//We start here by creating Riverpod ChangeNotifier class 
class StockPileNotifier extends ChangeNotifier {
  //Create Empty list to hold individual piles of which will hold data

  //List of stockpile
  final List<StockPile> _pile = [
    // StockPile(name: "Get 3 packs of chocolate ๐Ÿซ"),
  ];

  //Getter to get the amount of list in the _pile
  int get pileAmount => _pile.length;

  //Getter to get data from _pile of which can't be modified externally 
  UnmodifiableListView<StockPile> get pile => UnmodifiableListView(_pile);

  //Add a pile from the StockPile
  void addPile(StockPile pile) {
    _pile.add(pile);
    notifyListeners();
  }

  //Remove a pile from the StockPile
  void removePile(StockPile pile) {
    _pile.remove(pile);
    notifyListeners();
  }

  //Update an existing pile
  void updatePile(StockPile updatedPileItem) {
    //check if the updatedPile already exist in the _pile index
    final index = _pile.indexOf(updatedPileItem);

    final oldPileItem = _pile[index]; //get the old pile item to compare

    if (oldPileItem.name != updatedPileItem.name) {
      //if it's not equal; meaning they are different, thus update the value
      _pile[index] = oldPileItem.updated(updatedPileItem.name);
      notifyListeners();
    }
  }

//clear data from the _pile list
  void clearPile() {
    _pile.clear();  
    notifyListeners();
  }
}

The usage of the above-created provider would depend on your needs...but first, you need to remember to wrap wherever you want to pass data with a Consumer widget in other to access ref

   body: Scaffold(
          appBar: AppBar(
            centerTitle: true,
            elevation: 0,
            backgroundColor: const Color(0XFFFCFAFF),
            title: Text(
              "StockPile",
              style: GoogleFonts.raleway(
                textStyle: const TextStyle(color: Color(0xFF0C2539)),
                fontWeight: FontWeight.bold,
                fontSize: 24,
              ),
            ),
          ),
          body: Center(
            child: Consumer(builder: (context, ref, child) {
            //use ref.watch modifiers to check for changes in the provider
              final pileItemModel = 
              ref.watch(stockPileChangeNotifierProvider);
              return pileItemModel.pileItemAmount == 0
                  ? Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Center(
                              child: Text(
                                "Add a Pile to your StockPile",
                                style: GoogleFonts.raleway(
                                  textStyle:
                                      TextStyle(color: Color(0x690C2539)),
                                  fontWeight: FontWeight.bold,
                                  fontSize: 25,
                                ),
                              ),
                            ),
                          ],
                        ),

                      ],
                    )
                  : ListView.builder(
                      itemCount: pileItemModel.pileItemAmount,
                      itemBuilder: (context, index) {
                        final pileItem = pileItemModel.pile[index];
                        return Padding(
                          padding: const EdgeInsets.symmetric(
                              horizontal: 10, vertical: 5),
                          child: Dismissible(
                            key: Key(pileItem.name),
                            direction: DismissDirection.endToStart,
                            onDismissed: (direction) {
                              pileItemModel.removePile(pileItem);                              
                            },
                            background: Container(
                              padding:
                                  const EdgeInsets.symmetric(horizontal: 20),
                              decoration: BoxDecoration(
                                color: const Color(0xFFFFE6E6),
                                borderRadius: BorderRadius.circular(10),
                              ),
                              child: Row(
                                children: const [
                                  Spacer(),
                                  Icon(
                                    Icons.delete_outline,
                                    color: Color(0xB70C2539),                                    
                                  ),
                                ],
                              ),
                            ),
                            child: ListTile(
                              title: Padding(
                                padding:
                                    EdgeInsets.symmetric(horizontal: 20),
                                child: GestureDetector(
                                  onTap: () async {
                                    final updatedPileItem =
                                        await createAndUpdateDialog(
                                            context, "Update a Pile",
                                            pileItem);

                                    if (updatedPileItem!.name != "") {
                                      pileItemModel.update(updatedPileItem);
                                    }
                                  },
                                  child: Text(
                                    pileItem.displayName,
                                    style: GoogleFonts.raleway(
                                        textStyle: const TextStyle(
                                            color: Color(0xFF0C2539)),
                                        fontSize: 19,
                                        fontWeight: FontWeight.w500),
                                  ),
                                ),
                              ),
                            ),
                          ),
                        );
                      });
            }),
          ),

Let's keep in mind that using ChangeNotifierProvider is not an ideal way of doing all...Hence here is a refactored approach using StateNotifierProvider

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stockpile/model/stockpile.dart';

//PERFORMING CRUD OPERATION WITH STATE NOTIFIER

//Specify the provider which our app would access globally;
//Adding the type of the provider is also very helpful for readablity
final stockPileStateNotifierProvider =
    StateNotifierProvider<StockPileStateNotifier, List<StockPile>>((ref) {
  //We keep our state dynamic to take an empty List so data can be added to     
  //it;hence it can be mutable
  return StockPileStateNotifier([]);
});

//the "super" keyword must be stated because it is through it we pass data //into our state
//the "state" is basically any object hence it accepts list,map etc
//Add the object type to avoid any error later on

class StockPileStateNotifier extends StateNotifier<List<StockPile>> {
  StockPileStateNotifier(List<StockPile> state) : super(state);

  //Add to data to our state
  void addPile(StockPile pile) {  
  //spread operator to add the existing data and pass in new one to the state
    state = [...state, pile];   
  }

 //Remove an existing pile
  void removePile(StockPile pileToRemove) {   
    //locate the pile to remove
    state = state.where((pile) => pile != pileToRemove).toList();
  }

  //Update an existing pile
  void updatePile(StockPile updatedPileItem) {
    //to make an update; we check if the item already exists in our [state] 
    //and get the index
    final index = state.indexOf(updatedPileItem);
    //if it exists we get the actual data by accessing the state index
    final oldPileItem = state[index];
    if (oldPileItem.name != updatedPileItem.name) {
      //if it's not equal; meaning they are different, thus update the value
      state[index] = oldPileItem.updated(updatedPileItem.name);
    }
  }

  //clear pile ; By setting our state to be empty
  void clearPile() {
    state = [];
  }
}

Conversely the usage of StateNotiferProvider class within your application, but you must note that to access State and store it in ab object of the same datatype returned by the Consumer

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Consumer(builder: (context, WidgetRef ref, child) {
//Access the state of the stateNotifier and rebuild the widget when the state //changes
      List<StockPile> stockpile = ref.watch(stockPileStateNotifierProvider);
      ...

        body: Scaffold(
          appBar: AppBar(
            centerTitle: true,
            elevation: 0,
            backgroundColor: const Color(0XFFFCFAFF),
            title: Text(
              "StockPile",
              style: GoogleFonts.raleway(
                textStyle: const TextStyle(color: Color(0xFF0C2539)),
                fontWeight: FontWeight.bold,
                fontSize: 24,
              ),
            ),
          ),
          body: Center(
            child: stockpile.isEmpty
                ? Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Center(
                            child: Text(
                              "Add a Pile to your StockPile",
                              style: GoogleFonts.raleway(
                                textStyle:
                                    TextStyle(color: Color(0x690C2539)),
                                fontWeight: FontWeight.bold,
                                fontSize: 25,
                              ),
                            ),
                          ),
                        ],
                      ),                  
                    ],
                  )
                : ListView.builder(
                    itemCount: stockpile.length,
                    itemBuilder: (context, index) {
                      final pileItem = stockpile[index];
                      return Padding(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 10, vertical: 5),
                        child: Dismissible(
                          key: Key(pileItem.name),
                          direction: DismissDirection.endToStart,
                          onDismissed: (direction) {
                            //usage- delete Pile
                            stockpile.remove(pileItem);
                            setState(() {
                              if (stockpile.isEmpty) {
                                 //usage- clear Pile
                                stockpile.clear();
                              }
                            });
                          },           
                          child: ListTile(
                            title: Padding(
                              padding:
                                  const EdgeInsets.symmetric(horizontal: 20),
                              child: GestureDetector(
                                onTap: () async {
                                  final updatedPileItem =
                                      await createAndUpdateDialog(
                                          context, "Update a Pile", 
                                          pileItem);

                                  if (updatedPileItem!.name.isNotEmpty) {                                 
                                    //updates the selected stockpile
                                    setState(() {
                                      stockpile[index] = updatedPileItem;
                                    });
                                  }
                                },
                                child: Text(
                                  pileItem.displayName,
                                  style: GoogleFonts.raleway(
                                    textStyle: const TextStyle(
                                      color: Color(0xFF0C2539),
                                    ),                                   
                                  ),
                                ),
                              ),
                            ),
                          ),
                        ),
                      );
                    },
                  ),
          ),
        ),
  • Some handy modifiers used with Riverpod

Before I wrap this off, I would be explaining some commonly used modifiers in clear terms that you would encounter as your journey through with riverpod :

.read : It is used to read the current state of the value being provided from the provider, It is useful when you don't want to rebuild the UI but still want a data to be reflected on the UI, but if the data in the state changes later on it wont notify you or get updated in your app. Another instance would be when you want to trigger a particular functionality without having to rebuild the UI. the read modifer would come in handy.

.read is a synchronous operation that does not subscribe the widget to changes in the provider's state. It simply returns the current value of the state at the time it is called. Therefore, calling .read does not cause a widget rebuild.

.watch : It is an asynchronous operation, and It watches the data from the provider for changes and cross-checks to see if any change has been made to the data then rebuilds the widget to in order to create an update in the UI.

When you call .watch on a provider, the widget that calls it will be subscribed to changes of that provider. Whenever the provider's state changes, any widgets that are subscribed to it will be rebuilt.

.autoDispose : Resets any value stored in the provider to the default value when the screen pops off or when disposed within the App life cycle

.listen : This modifiers helps in listening to provider value in order to perform specific action based on listened value.

.notifiers : This modifier is specially used when working with a StateNotifierProvidier to help us to create a simple and consistent way to manage state and access functions within the StateNotifier class, it also helps us in being specific about which function we expose from the StateNotifier class.

There you have it, With this we've been able to breakdown some technicalities associated with this important topic as you look dive deeper in Application Development with Flutter

The above snippets are used in my Open Source Catalog Application (StockPile) of which was inspired by @OjoSaanu of which I built, hopefully some cool features would be added you get to see it playstore soon

This article wont be complete without appreciating the Flutter community and also some helpful colleagues @xclusivecyborg , @popestrings from the Flutter byte community who assisted to simplifying it all for me while it felt so overwhelming at the beginning and most importantly for coming by to give this a read, the Project Repo for you to practice can be found here.

Do have fun exploring it and you find incase of any bugs lurking around do well to leave a suggestion or a PR.... that's my piece on this topic, Now go forth and dive further into the docs and conquer more technical concepts and challenges with confidence !"... Cheers ๐Ÿค—

ย