Flutter Advanced - Elegant State Management | Introduction to Provider

Flutter Advanced - Elegant State Management | Introduction to Provider

State management that is recommended by Flutter and Google!

We have come pretty far in the series and cheers to you 🍻if you have been following along. This is the article from where the actual fun begins! Until now we have only seen states that lived in a widget or two.

This is where we make the state go global, i.e, lifting the state up as Flutter calls it! We will now deal with app-wide state and manage it as elegantly as possible. To do this let me introduce you to Provider.

The Problem Statement

Imagine you are building a Flutter project having a considerable amount of widgets. In your widget tree if any child widget needs some data you need to provide it through its constructor.

If the data lives at the top of the widget tree and it needs to be provided down to the child then you would have to pass it down through all the widgets via their constructors. That would lead to very cluttered and big constructors expecting a lot of data even if the data is not required by that particular widget but all it needs to do is pass that down to its child.

This approach will lead to unnecessary rebuilds because some data changed which we discussed in the previous articles.

This would lead to a very cluttered and messy code which in turn would result in error-prone and non-maintainable code. This is where the Provider comes into play. But before we begin let's see what other reasons are there to choose Provider as our state management tool when there are other options available.

Why Provider?

  • No nonsense boilerplate code setup required.

  • Complete separation of logic from the widgets.

  • Easy to grasp and beginner-friendly. Even a beginner in Flutter can understand what is happening behind the scenes.

  • Clean project directory structure.

  • It is recommended by Google and Flutter officially. This might be enough😂.

You will see the beauty of Provider in this article as well as the upcoming articles. So, with all that let's get familiar with the provider.

What is Provider?

A wrapper around InheritedWidget to make them easier to use and more reusable.

This is what the official definition says. Let me explain in detail. The provider is a package that gives us a pipeline to which we can connect our widgets. Consider providers like fuel pumps/gas stations and the widgets as vehicles. Here the fuel is data!

All the widgets that need data(vehicles that need fuel refill) will go to the provider(gas station) and connect with a hose to refill and those widgets(vehicles) which don't need fuel(data) will simply pass.

This is how the provider works! It simply provides data to those certain widgets that need it and doesn't even care about the widgets that don't need that particular data.

That's why the definition says that it is a wrapper that wraps around your widget as a pipeline and provides the required data.

For a clearer understanding consider the example below,

provider-flutter-example-by-shashank-biplav.gif

  • Here, we have a widget tree with the widget MyApp at the top which is the parent of all the child widgets.

  • The provider simply consists of data/state and we attach the provider to a widget which in this case is at the top of the widget tree, i.e, MyApp.

  • As soon as the provider is attached to any widget, all the child widgets can listen to that provider's data/state.

  • Here we can see that the 1st and 3rd child widgets are listening to the provider whereas the second widget is not!

  • As soon as there is any change in the data/state in the provider the build(){...} method of the 1st and 3rd child widgets will be executed and those widgets will be rebuilt.

  • The 2nd child widget will not rebuild unnecessarily during the build process of 1st and 3rd child widgets.

  • The child widgets can be stateful or stateless, it doesn't matter because the data is provided externally by the provider so they will rebuild as soon as the data changes.

The data is passed from the provider to the widgets not by the constructors in the widget tree but by making those widgets an active listener or consumer of the provider.

This is how Providers work, so let's see them in action!

Installation

Installing Provider in your app is just adding the following to your pubspec.yaml file and running flutter pub get from your terminal,

dependencies:
  provider: ^4.3.3

Using Providers

Now we will implement a provider in our Flutter app but before that let's see the file structure that we are using,

>android
>build
>ios
>lib 
   -> helpers
   -> controllers
   -> providers
         -> authentication_provider.dart
         -> recipes_provider.dart
         -> chef_provider.dart
   -> screens
         -> home_screen.dart
         -> authentication_screen.dart
   -> widgets
   -> models
   -> main.dart
>test
>.gitignore
>pubspec.lock
>pubspec.yaml
>README.md

Create a separate folder for all the providers that you will create so that all the logic for handling state and data will reside inside a single folder separated from our other widgets. All the widgets that returns thescaffold widget is a separate screen in Flutter hence we separate those widgets from other widgets. All other widgets will reside inside the widgets folder.

There can be more nested folders if we would have a more larger and complex app. For example, all the providers that handle user authentication may live inside a separate folder inside the providers folder and so on.

This is the basic example of a folder structure that is heavily used in production-level apps for easier code readability and maintenance.

Creating your first Provider

Create a dart file inside the providers folder with a name relevant to the type of data that your provider is storing. In my case, I am storing all the recipes that are fetched from a REST API.

Creating a model

Also, we will use the MVC design pattern here. So, I have created a Recipe model in the models folder named recipe.dart. The model recipe.dart looks like this,

import 'package:flutter/foundation.dart';

class Recipe {
  final String id;
  final String title;
  final String imageUrl;
  final int duration;
  final List<dynamic> ingredients;
  final List<dynamic> categories;
  final List<dynamic> steps;
  final String chef;
  final String chefName;
  final String chefImageUrl;
  final String complexity;
  final String affordability;
  final bool isVegetarian;

  Recipe(
      {@required this.id,
      @required this.title,
      @required this.imageUrl,
      @required this.duration,
      @required this.ingredients,
      @required this.categories,
      @required this.steps,
      @required this.chef,
      @required this.chefName,
      @required this.chefImageUrl,
      @required this.complexity,
      @required this.affordability,
      @required this.isVegetarian});
}

Above you can see that we have defined a recipe model that holds all the instance variables that we need. Also, we have a constructor which enforces a @required decorator. This decorator is made available to us by the foundation.dart package. It is also available in material.dart but we don't need any StatelessWidget or StatefulWidget class hence we use this foundation.dart package.

You might ask how did you model this class? Well here is a snapshot of the data incoming from the REST API,

{
    "message": "Recipes Fetched Successfully.",
    "recipes": [
        {
            "ingredients": [
                "Milk: 180 ml",
                "Sugar: 2 Tbsp",
                "Instant Yeast: 2 Tsp",
                "Plain Flour/Maida: 2 Cups",
                "Salt: 1/2 Tsp",
                "Room-temp Butter: 5tbsp",
                "Milk & Butter: For Brushing",
                "Oil: For greasing"
            ],
            "categories": [
                "5fa56a86240c6d54b32f5663",
                "5fa56e3c240c6d54b32f5667",
                "5fa77dc3bc146402c599efb6"
            ],
            "steps": [
                "Pour a lukewarm milk to a cup and, add and sugar, yeast.",
                "Mix well and keep aside for 10-15 minutes.",
                "Add the flour and salt in a bowl.",
                "After 10 minutes yeast mixture should look frothy.",
                "Add this mix to flour mixture and make a dough.",
                "It would be on the sticky paste side. Its absolutely perfect.",
                "Now add the butter to the dough and keep kneading for 15-20 mins.",
                "Do not add extra flour to the dough.",
                "After that transfer the dough to a big greased bowl and cover the bowl.",
                "Keep it aside for 60-90 mins until it is double in size.",
                "After the dough is fermented, take some maida dust your hands and punch down the dough.",
                "ake out and knead for 5 mins.",
                "Now divide the dough in equal potions and shape the dough.",
                "Now grease the pan and place the shaped dough.",
                "Cover the tin with damp cloth and keep aside for another 40-45 mins.",
                "Carefully remove the damp cloth after 30 minutes so that they don’t stick to the cloth. After 45 minutes.",
                "Brush the pav gently with milk.",
                "Gently place the cake tin in the oven and bake for 15 minutes at 200 C.",
                "Take them out immediately and brush the top part with butter.",
                "Wallah! Have it plain in your breakfast or with bhaji in your dinner."
            ],
            "_id": "5fa77fc6bc146402c599efb8",
            "title": "Dinner Roll / Pav",
            "imageUrl": "images/2020-11-08T05:19:01.845Z-pav.jpg",
            "duration": 250,
            "chef": {
                "_id": "5fa77f33bc146402c599efb7",
                "name": "Nidhi Saha",
                "profileImageUrl": "images/2020-11-08T05:16:35.202Z-nidhisaha.jpg"
            },
            "complexity": "SIMPLE",
            "affordability": "AFFORDABLE",
            "isVegetarian": true,
            "createdAt": "2020-11-08T05:19:02.158Z",
            "updatedAt": "2020-11-08T05:19:02.158Z",
            "__v": 0
        }
    ],
    "totalItems": 1
}

In case you want to try the REST API yourself, here is the endpoint to fetch all the recipes,

https://bakeology-alpha-stage.herokuapp.com/user/recipes

This is how incoming data is mapped using a model! With this in place let's now create a provider to store all the incoming data.

Creating a provider

I am naming my provider as recipe_provider.dart. Here is the code that makes up a provider,

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

import '../models/recipe.dart';

class RecipeProvider with ChangeNotifier {
  List<Recipe> _recipes = [];

  List<Recipe> get recipes {
    return [..._recipes];
  }

  Future<Recipe> fetchAndSetRecipes() async {
    const url = 'https://bakeology-alpha-stage.herokuapp.com/user/recipes';
    try {
      final response = await http.get(url);
      final extractedData = json.decode(response.body) as Map<String, dynamic>;
      if (extractedData == null) {
        return null;
      }
      final List<Recipe> loadedRecipes = [];
      extractedData["recipes"].forEach((recipeData) {
        loadedRecipes.add(
          Recipe(
              id: recipeData["_id"],
              title: recipeData["title"],
              duration: recipeData["duration"],
              imageUrl: recipeData["imageUrl"],
              affordability: recipeData["affordability"],
              isVegetarian: recipeData["isVegetarian"],
              steps: recipeData["steps"],
              categories: recipeData["categories"],
              chef: recipeData["chef"]["_id"],
              chefName: recipeData["chef"]["name"],
              chefImageUrl: recipeData["chef"]["profileImageUrl"],
              complexity: recipeData["complexity"],
              ingredients: recipeData["ingredients"]),
        );
      });
      _recipes = loadedRecipes;
      notifyListeners();
    } catch (error) {
      print(error);
      throw error;
    }
  }
}

Now let's see what is happening inside this RecipeProvider.

Imports

We import the material.dart package which gives us this ChangeNotifier mixin. Also, we need dart:convert which is a conversion utility package containing encoders and decoders for converting between different data representations, for example, converting JSON data.

Also, install the http package by including this in your pubspec.yaml with proper spacing/indentation.

  http: ^0.12.2

We need this package to hit the endpoint of our REST API. Also, we import our recipe model into this file.

Adding ChangeNotifier

ChangeNotifier is a mixin which is provided to us by the material.dart package. A mixin is similar to extending another class but the difference is that instead of inheriting from the class some properties of the mixin are merged with our defined class. Hence we use the with keyword to add the ChangeNotifier mixin to our RecipeProvider class.

Instance Variables and Getter Methods

To make sure that our list of recipes that we fetch from the REST API is immutable from outside this RecipeProvider class we declare a list of recipes(class defined by us) which is just an empty array and is a private variable hence an _ (underscore) is used.

This empty array will accept the values which fulfill the criteria of being a Recipe. Recipe class is defined by us! So each element in this list (which is just an array) will be of type Recipe.

As the list, i.e, variable _recipe is private we define a getter method so that we can get all the recipes. This getter destructures the _recipes variable and returns a new array with the same Recipe elements thus making the original List immutable.

Function to fetch data from REST API

Now we come to the function fetchAndSetRecipes() async {...} which eventually returns the Future. Being a generic type we specify that this function will finally resolve into Recipe in the future hence Future<Recipe>.

All the fetching logic is wrapped inside a try{...} catch(error){...} block to handle any errors from the backend, i.e, REST API.

Inside the try{...} block, firstly we fetch all the recipes which are initially in JSON format. Then we use json.decode(response.body) method to decode the response body finally converting it into a Map which is also a generic type. The keys in the Map are strings and values are dynamic because values can be anything, i.e, arrays, strings, numbers, etc. Hence we decode the response body as Map<String, dynamic>.

Once the data has been decoded and converted to Map, then we define another List<Recipe> which is declared final and is initially empty. We will initially store the fetched recipes in this list and later when the recipes are finalized we transfer this to our original private _recipes variable.

Then we insert elements of type Recipe by looping through each and every recipe from the end and converting it into our own type Recipe. Once this process completes and the loadedRecipes variable is full we assign all the data inside it to our private variable _recipes.

notifyListeners() function

After we are done with all the fetching and mapping of data we use notifyListeners() to notify all the widgets that are consumers or listeners to this provider. This ensures that once the data inside our private variable _recipes change all the widgets that are consumers/listeners will also rebuild reflecting the change in data.

That is all we need to set up a Provider but we are not done yet! Let's now see how we can hook up widgets to our provider.

Linking Widgets to Provider

First of all we need to attach our provider to a widget that is a parent to the widgets in which we need the provider data. You can attach the provider anywhere but keep in mind that the attached widget must be at the highest possible point in the widget tree of interested widgets! In this case, we attach the RecipeProvider in our MyApp widget that is a parent to all the widgets. So our main.dart file looks like this.

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

import './providers/recipe_provider.dart';

import './screens/home_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
          create: (ctx) => RecipeProvider(),
          child: MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Provider App Demo',
          theme: ThemeData(
            primarySwatch: Colors.blueGrey,
            accentColor: Colors.blueGrey[300],
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: HomeScreen(),
         ),
      ),
    );
  }
}

This is how we attach a provider to the highest possible point in the widget tree of interested widgets using ChangeNotifierProvider. We will discuss more about this in the upcoming articles but for now, let's proceed to the HomeScreen and RecipeList widgets.

Consider a HomeScreen widget which is a stateless widget but holds another custom widget RecipeList. The home_screen.dart looks like this,

import 'package:flutter/material.dart';

import '../widgets/recipe_list.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar:AppBar(
        title:Text('All Recipes'),
        centerTitle: true,
      ),
      body: Column(
        children: [
          SizedBox(height: 20),
          RecipeList(),
        ],
      ),
    );
  }
}

and the recipe_list.dart looks like this,

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

import './recipe_list_item.dart';

import '../providers/recipe_provider.dart';

class RecipeList extends StatefulWidget {
  @override
  _RecipeListState createState() => _RecipeListState();
}

class _RecipeListState extends State<RecipeList> {
  bool _isLoading = false;
  @override
  void initState() {
    setState(() {
      _isLoading = true;
    });
    Provider.of<RecipeProvider>(context, listen: false)
        .fetchAndSetRecipes()
        .then((_) {
      setState(() {
        _isLoading = false;
      });
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final recipeData = Provider.of<RecipeProvider>(context);
    final fetchedRecipes = recipeData.recipes;
    return _isLoading
        ? Center(
            child: CircularProgressIndicator(),
          )
        : Container(
            child: ListView.builder(
              shrinkWrap: true,
              itemCount: fetchedRecipes.length,
              itemBuilder: (context, index) => Padding(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
                child: RecipeListItem(
                  recipeId: fetchedRecipes[i].id,
                  recipeTitle: fetchedRecipes[i].title,
                  recipeImageUrl: fetchedRecipes[i].imageUrl,
                  chefName: fetchedRecipes[i].chefName,
                  chefImageUrl: fetchedRecipes[i].chefImageUrl,
                  isVegetarian: fetchedRecipes[i].isVegetarian,
                  duration: fetchedRecipes[i].duration,
                ),
              ),
            ),
          );
  }
}

How the RecipeList widget is linked to the RecipeProvider?

  • First of all RecipeList is a StatefulWidget. Apart from the provider this widget also has its own internal state! This internal state determines what is eventually displayed on the screen. If the recipes are being fetched from the REST API then the _isLoading is true hence a CircularProgressIndicator() is displayed and if the loading of the recipes has completed then the recipe list is displayed.

  • Now, initState(){...} method executes whenever the RecipeList widget is about to be rendered on the screen. It executes just before the render and after the build(){...} method is executed! Hence this method is perfect to execute our fetchAndSetRecipes() async {...} function to fetch all the recipes from our REST API.

  • To attach an active listener we use,

Provider.of<RecipeProvider>(context)....

This attaches an active listener to our provider and will trigger the build(){...} method of the widget whenever the data inside the provider changes.

Notice that inside the initState(){...} method we have a Provider but we have explicitly declared listen: false because we cannot attach an active listener inside this method.

Also we don't need an active listener inside the initState(){...} method because we just need to call fetchAndSetRecipes() async {...} function!

  • The active listener in our widget is recipeData which is of type final. Since this variable lives inside the build(){...} method of our widget, any change in the data inside the provider will trigger the build(){...} method of our widget.

  • Next, we use the getter method, that we defined in our provider to get all the recipes! The getter methods in Dart can be simply called by their name hence, recipeData.recipes calls the getter method which gives us the recipes inside our widget.

  • Once the data is available inside the widget you can display or pass the data along to another widget as per your requirement! In my case, I passed the data to another widget RecipeListItem which will display the individual recipes beautifully in the end.

This is all you need to do for adding provider as your state manager in your app! This was pretty long but still a lot of fun. I guess you are seeing the bigger picture here.

In case the data inside the RecipeProvider changes only the RecipeList widget and its child, i.e, RecipeListItem will rebuild. The HomeScreen specifically the AppBar and the SizedBox widgets will not rebuild🤯.

Conclusion

I hope that you can already see the benefits of using provider in the long run. There is no boilerplate code setup required and it also prevents unnecessary widget rebuilds. In short, it is always a win-win situation here.

I hope this makes provider your go-to friend when tackling state management in flutter. If you have any questions be sure to ask in the comments. Also if you liked this beginner-friendly approach of mine to help you understand provider be sure to smash those emojis on this article.

It is the way I tried understanding the provider a couple of years ago! I wish I had this post back then to save me all the hassle! Just kidding🤪. In the upcoming articles, we will see more providers in action. Till then, Adios 🤟🏽✌🏽🙌Devs and Happy Coding👨🏽‍💻👩‍💻!