# How To Use Providers For Enhanced State Management

We often use providers to manage the state of our application. Every provider can perform multiple tasks and each task can have multiple statuses at the time of execution which makes it challenging for UI developers to handle various states. 

In this article, we will be creating a wrapper around **providers** and **consumers** which will help us assign a unique name to each task. This can later  be used to update their statuses (For example: idle , loading , done, etc) and respective data while also notifying all consumers.

### Prerequisites 

To start off, we need to add a **provider** package in `pubspec.yaml ` using the code given below:


```
dependencies:
  provider: ^5.0.0
``` 



## Creating wrapper for provider

To design a wrapper for providers, we will create a `BaseModel` class which has three fields, namely **status**, **data** and **error**. These three fields are used to store updates of the task to their respective task names. Use the code below to execute this:


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

class BaseModel with ChangeNotifier {
  Map<String, dynamic> data = <String, dynamic>{};
  Map<String, Status> status = {"main": Status.Idle};
  Map<String, String> error = {};

  setStatus(String taskName, Status _status) {
    this.status[taskName] = _status;
    notifyListeners();
  }

  setData(String taskName, dynamic _data) {
    this.data[taskName] = _data;
  }

  setError(String taskName, String _error, [Status _status]) {
    if (_error != null) {
      error[taskName] = _error;
      status[taskName] = Status.Error;
    } else {
      this.error[taskName] = null;
      this.status[taskName] = _status ?? Status.Idle;
    }
    notifyListeners();
  }

  reset(String taskName) {
    this.data?.remove(taskName);
    this.error?.remove(taskName);
    this.status?.remove(taskName);
  }

  notify() {
    notifyListeners();
  }
}

enum Status { Idle, Loading, Done, Error }

``` 
Apart from these fields, we have also have many statuses and functions to update or reset it. If we notice closely, all of the functions(`setStatus`, `setError`) accepts `string taskName` followed by the value for the task; this value will be set for a particular `taskName`.  In the case of a reset, we only require to call the `taskName` and this will simultaneously remove the data associated.


## Creating a wrapper for consumers 

To create a wrapper for consumers, the `ProviderHandler` should be designed in such a way that it can handle all the statuses and update the UI accordingly. The `ProviderHandler` should have the following parameters:


- `taskName` : Unique name for the task 

- `load` :  This function is the task that we want to perform 

- `successBuilder` : This function will be executed when the task’s status is set to Done  

- `errorBuilder` : This function will be executed when the task’s status is set to Error 

- `loaderBuilder` : This function will be executed when the task’s status is set to Loading  

- `showError` : A bool variable to decide whether to show an error dialog or not 

Let's see how the coding is executed:

```

class ProviderHandler<T extends BaseModel> extends StatelessWidget {
  final Widget Function(T) successBuilder;
  final Widget Function(T) errorBuilder;
  final Widget Function(T) loaderBuilder;
  final String taskName;
  final bool showError;
  final Function(T) load;

  const ProviderHandler(
      {Key key,
      this.successBuilder,
      this.errorBuilder,
      this.taskName,
      this.showError,
      this.loaderBuilder,
      this.load})
      : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Consumer<T>(builder: (context, _provider, __) {
      if (_provider?.status[taskName] == Status.Loading) {
        return loaderBuilder != null
            ? loaderBuilder(_provider)
            : Center(
                child: Container(
                  height: 50,
                  width: 50,
                  child: CircularProgressIndicator(),
                ),
              );
      } else if (_provider?.status[taskName] == Status.Error) {
        if (showError) {
           ErrorDialog()
               .show(_provider.error[taskName].toString(), context: context);
          _provider.reset(taskName);
          return SizedBox();
        } else {
          _provider.reset(taskName);
          return errorBuilder(_provider);
        }
      } else if (_provider?.status[taskName] == Status.Done) {
        return successBuilder(_provider);
      } else {
        WidgetsBinding.instance.addPostFrameCallback((_) async {
          await load(_provider);
        });
        return Center(
          child: Container(
            height: 50,
            width: 50,
            child: CircularProgressIndicator(),
          ),
        );
      }
    });
  }
}

``` 


`ProviderHandler` requires a class parameter -**T**, which is a subclass of `BaseModel`.


All the providers that we want to use must extend to the `BaseModel`.


If we look at the definition of `ProviderHandler`, it states that the function is a stateless widget that returns a consumer which reruns the builder whenever `notifyListeners()` is called. In the builder method, function parameters are executed according to a task's status. When status is set to:

1.  **Loading** -> `loaderBuilder` get invoked
2.  **Error** -> `errorBuilder` get invoked
3.  **Done** -> `successBuilder` get invoked
4. When none of the statuses match, **load** gets invoked to execute the task.

![ezgif.com-gif-maker.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1621510529804/96Tjr4GYw.gif)

Now that our wrapper classes are in place, we can start creating providers and using them with the help of `ProviderHandler`. 

## Demo App

In this article we will be creating a news app using which we will be getting data through an API and displaying the result. You can find the code [here](https://github.com/knowsach/daily-news/tree/master).  

This news app can have following statuses:- 

- **Loading** : Denotes data is being fetched

- **Done** : Shows we successfully get the data

- **Error** : If we encounter an error

Let's proceed with the coding:

```
class NewsProvider extends BaseModel {
  List<Article> todaysNews = [];

  // operations
  String GET_DATA = 'get_data';

  Future getNewsData({String country = 'in'}) async {
    setStatus(GET_DATA, Status.Loading);
    try {
      Response response = await get(
          '${Constants().api}country=$country&apiKey=${Constants().apiKey}');
      Map data = jsonDecode(response.body);

      data.containsKey('articles')
          ? data['articles']
              .forEach((article) => {todaysNews.add(Article.fromJson(article))})
          : todaysNews = [];

      setData(GET_DATA, todaysNews);
      setStatus(GET_DATA, Status.Done);
    } catch (e) {
      setError(GET_DATA, e.toString());
      setStatus(GET_DATA, Status.Error);
    }
  }
}
``` 


- We will come across a unique task name called `GET_DATA` in the `NewsProvider` class which we will associate to the `getNewsData` function to store the status, data and error. We will set the status as loading at the beginning of the function.

- If the network call was successful and data has been stored in the `todaysNews` array, we can manually set the data and status to `todaysNews` and Done respectively.

- If the error is caught, we can send an error message and set the status to Error.

- Whenever we are executing `setStatus`, it internally calls `notifyListeners()` which will rerun the consumer builder associated with the particular `NewsProvider` and as per the latest status while rendering the UI.

Here is a pictorial depiction of the output:

![fetchNewsGif.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1622750563455/RbFdg0m6V.gif)


#### To add a function to like an article in `NewsProvider`:

Use the code given below to do this:

```
class NewsProvider extends BaseModel {
.
.
.

  String LIKE_ARTICLE = 'like_article';

  likeNewsArticle(int index) async {
    setStatus(LIKE_ARTICLE, Status.Loading);

    if (index > todaysNews.length) {
      setStatus(LIKE_ARTICLE, Status.Error);
      setError(LIKE_ARTICLE, 'index not in range');
      return;
    }
    todaysNews[index].isFav = !todaysNews[index].isFav;
    setStatus(LIKE_ARTICLE, Status.Done);
  }
}

``` 

Now we have another unique task name, i.e., `LIKE_ARTICLE`, to store the status of `likeNewsArticle` function. Like the previous function, we can update the status from Loading to Error or Done here too.


We have our provider ready! Let's use it to fetch data using `ProviderHandler` as shown below:


```
  ProviderHandler<NewsProvider>(
                key: UniqueKey(),
                taskName: NewsProvider().GET_DATA,
                showError: false,
                load: (provider) => provider.getNewsData(),
                errorBuilder: (provider) {
                  return Center(
                      child: Container(
                          child:
                              Text(provider.error[provider.GET_DATA])));
                },
                successBuilder: (provider) {
                  return ListView.builder(
                    itemCount: provider.data[provider.GET_DATA].length,
                    itemBuilder: (context, index) {
                      return Column(
                        children: [
                          NewsCard(
                              provider.data[provider.GET_DATA][index],
                              index: index,
                              isNewsDesc: false),
                        ],
                      );
                    },
                  );
                },
              )
``` 

Here, we have given all the parameters, i.e., `taskName`, `load, `errorBuilder` and `successBuilder` to fetch the news data. We are passing `getNewsData` while loading, where we have defined the logic to get news data by setting `GET_DATA` as index. Whenever there is a status update,  the UI is alternated between `loaderBuilder`, `errorBuilder` or `successBuilder`, depending upon the status.


### Need for a Provider Callback

Our `ProviderHandler` works perfectly for handling all the statuses and showing a widget at the end; but, there are several scenarios where we just want to perform a task but don't need a widget on completion! In this case we can make use of something called  a`ProviderCallback` which is very similar to `providerHandler` except that it does not returns a widget. Let's look at the code for this which is given below:



```
Future providerCallback<T extends BaseModel>(BuildContext context,
    {@required final Function(T) load,
    @required final String Function(T) taskName,
    @required Function(T) onSuccess,
    bool showDialog = true,
    bool showLoader = true,
    Function(T) onErrorHandeling,
    Function onError}) async {
  final T _provider = Provider.of<T>(context, listen: false);
  String _taskName = taskName(_provider);

  if (showLoader) LoadingDialog().show();
  await Future.microtask(() => load(_provider));
  if (showLoader) LoadingDialog().hide();
 
  if (_provider.status[_taskName] == Status.Error) {
    if (showDialog) {
      ErrorDialog().show(
        _provider.error[_taskName].toString(),
        context: context,
        onButtonPressed: onErrorHandeling,
      );
    }

    if (onError != null) onError(_provider);

    _provider.reset(_taskName);
  
  } else if (_provider.status[_taskName] == Status.Done) {
    onSuccess(_provider);
  }
}

``` 

`ProviderCallback` is based on the same concept as that of `providerHandler`. We are accepting a `taskName` depending upon the status when we are invoking the function.

In the case you wish to use `providerCallback` to like a news article, the code for this process is given below:


```
providerCallback<NewsProvider>(
                    context,
                    load: (provider) => provider
                              .likeNewsArticle(widget.index),
                    taskName: (provider) =>
                               provider.LIKE_ARTICLE,
                    showLoader: true,
                    showDialog: true,
                    onErrorHandeling: (provider) {
                         print( 'error: ${provider.error[provider.LIKE_ARTICLE]}');
                       },
                    onSuccess: (provider) {
                     print('article liked');
                 });
``` 
## Conclusion
We have already defined this `likeNewsArticle` function in the `NewsProvider` segment. A pictorial representation of the output is given below:

![likeNewsArticle.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1622751033071/NvN0OlTtP.gif)


You can find the code used in this article [here](https://github.com/knowsach/daily-news/tree/master). I hope this article has made the topic of integrating for better state management much more simple.

Thanks for reading!

