Provider State Management

Abubakar Saddique
13 min readJan 3, 2024

--

What is Provider ??

A Provider is the simplest technique for the global state management .

It is the advanced form of the inherited widget .

Provider is the third party package or the library that is used as a simplest technique for the global state management .

The introduction with the provider is given at the official documentation .

In the official documentation they describe the provider by taking the example of the add to cart but in my case i will start will the basic example of the counter .

So let’s begin :

For that purpose you must import the provider or add the dependencies of that package .

You must add the above dependencies in your app to use the provider package .

i ) Change Notifier :

According to the official documentation .

A class that can be extended or mixed in that provides a change notification API using [VoidCallback] for notifications.

This class will notify it’s listener when there is some change in that specific class .

For that purpose you have to make a custom notifier class and extend that class with the change notifier class to listen if there is any change in that specific class .

In our case of counter i make a counter notifier class and extend it with the change notifier class .

Make a private member of the variable that you wan’t to change and assign it an initial value .

In our case the counter text will be changed every time when the user click’s on the floating action button . So we have to make a counter variable and make it as a private member of the counter notifier class .

  int _counter = 0;

Now make the public getter and the setter of that variable .

int get counter => _counter;

set counter(int value) {
counter = value;
}

Now you have to make the function’s of the all of the possible operation’s . In our case we have two posiblities that are the counter Increment and the counter decrement .

void increment() {
_counter++;
notifyListeners();
}

void decrement() {
_counter--;
notifyListeners();
}

Above you have note one thing that i have use notify listener in both of the function’s or operation’s .

NotifylListener’s are used tells the widgets that are listening to this model to rebuild.

This will notify it’s listener’s and to rebuild the given widget when there is some change in that widget .

According to the official documentation .

Call this method whenever the object changes, to notify any clients the object may have changed. Listeners that are added during this iteration will not be visited. Listeners that are removed during this iteration will not be visited after they are removed .

The complete code of the custom change notifier class is given below .

import 'package:flutter/material.dart';


class CounterNotifier extends ChangeNotifier {

int _counter = 0;

int get counter => _counter;

set counter(int value) {
counter = value;

notifyListeners();
}


void increment() {
_counter++;
notifyListeners();
}

void decrement() {
_counter--;
notifyListeners();
}
}

That’s all for our notifier class .

Now let’s move forward .

Change Notifier Provider :

Change notifier provider is a widget that link’s the our custom change notifier class and the widget’s or the UI .

According to the official documentation .

ChangeNotifierProvider is the widget that provides an instance of a ChangeNotifier to its descendants. It comes from the provider package.

Listens to a ChangeNotifier, expose it to its descendants and rebuilds dependents whenever ChangeNotifier.notifyListeners is called.

For detailed information about the change notifier class . Kindly read it from the official documentation link is given above .

Use the change notifier provider on the top of the widget tree .

In the create method use the custom notifier class.

The page on which the ui changes will comes as the child of the change notifier provider .


home: ChangeNotifierProvider(
create: (context) => CounterNotifier(),
child: const MyHomePage(title: 'Flutter Demo Home Page')),

If you are using more then one provider then use the muti provider instead of the simple change notifier provider and pass a list of the your all change notifier provider’s as the property of the provider’s of the chnage notifier provider

Here you can add as many provider’s as needed .

 home: MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => CounterNotifier(),
child: const MyHomePage(title: 'Flutter Demo Home Page')),
ChangeNotifierProvider(
create: (context) => AlbumNotifier(),
child: const MyHomePage(title: 'Flutter Demo Home Page'),
)
],
),

Now it’s time to perform the increment and decrement operation now on the click event of the button’s i will perform that operation’s .

According to the official documentation .

Sometimes, you don’t really need the data in the model to change the UI but you still need to access it .

For this use case, we can use Provider.of, with the listen parameter set to false.


void _incrementCounter() {
Provider.of<CounterNotifier>(context, listen: false).increment();
}

void _decrementCounter() {
Provider.of<CounterNotifier>(context, listen: false).decrement();
}

The provider . of method is of the inherited widget type . Must pass your custom notifier class as the parameter of the of method of the provider and make it’s listen false and then call the function that you make in that specific custom notifier class .

Now call these function’s on the click event’s of the button’s .

Consumer Widget :

To access the value of the custom notifier class you should use the consumer widget for that purpose .

Provide your customNotifier class as the template type of the consumer to fetch your data .

According to the official documentation .

Provider is based on types, and without the type, it doesn’t know what you want.

It is best practice to put your Consumer widgets as deep in the tree as possible. You don’t want to rebuild large portions of the UI just because some detail somewhere changed .

According to the official documentation .

The only required argument of the Consumer widget is the builder. Builder is a function that is called whenever the ChangeNotifier changes. (In other words, when you call notifyListeners() in your model, all the builder methods of all the corresponding Consumer widgets are called.) .

Obtains Provider<T> from its ancestors and passes its value to builder.

This will fetch and value of the counter and the builder method will rebuild that widget when there the notify listener is called on there is some change in that widget here in our case the text will be rebuild when the increment and the decrement function’s will be called .

 Consumer<CounterNotifier>(
builder: (context, value, child) =>
Text(value.counter.toString()),
),

The second argument of the builder function is the instance of the ChangeNotifier that will provide the value of the counter .

That’s all for the provider state management .

The complete code of the ui is given below .

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


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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),

home: ChangeNotifierProvider(
create: (context) => CounterNotifier(),
child: const MyHomePage(title: 'Flutter Demo Home Page')),


);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
void _incrementCounter() {

Provider.of<CounterNotifier>(context, listen: false).increment();
}

void _decrementCounter() {
Provider.of<CounterNotifier>(context, listen: false).decrement();
}


@override
Widget build(BuildContext context) {

return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Consumer<CounterNotifier>(
builder: (context, value, child) =>
Text(
value.counter.toString(),
style: const TextStyle(
fontSize: 22,
color: Colors.brown,
fontWeight: FontWeight.bold),
),
),
],
),
),
floatingActionButton: ButtonBar(
children: [
FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
const SizedBox(
width: 40,
),
FloatingActionButton(
onPressed: _decrementCounter,
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
const SizedBox(
width: 40,
),
],
),
);
}
}


The Provider is based on the types and it has some of the issues for that purpose we use the riverpod and the riverpod is the advanced form of the provider and i recommend you to use the riverpod then the provider .

That’s all for the counter provider .

Now let’s do the API with the provider .

If you don’t know how to do api’s . So first i recommend you to read my article on Application Programming Interface (API) .

For that purpose you have the http package to interact with the api’s .

Here i use the fake api’s of json place holder .

This website will provide multiple types of the free fake api’s . but i will perform the album api .

Url : https://jsonplaceholder.typicode.com/albums .

Now let’s begin .

Now first you have to add this package of http in your app .

After that you can start the process of fetching api .

i ) Model Class :

Make a model class for your api’s . Here i am using the album api’s so i have to make an album model class .

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';

class AlbumApiModel {
int userId;
int id;
String title;
AlbumApiModel({
required this.userId,
required this.id,
required this.title,
});

AlbumApiModel copyWith({
int? userId,
int? id,
String? title,
}) {
return AlbumApiModel(
userId: userId ?? this.userId,
id: id ?? this.id,
title: title ?? this.title,
);
}

Map<String, dynamic> toMap() {
return <String, dynamic>{
'userId': userId,
'id': id,
'title': title,
};
}

factory AlbumApiModel.fromMap(Map<String, dynamic> map) {
return AlbumApiModel(
userId: map['userId'] as int,
id: map['id'] as int,
title: map['title'] as String,
);
}

String toJson() => json.encode(toMap());

factory AlbumApiModel.fromJson(String source) =>
AlbumApiModel.fromMap(json.decode(source) as Map<String, dynamic>);

@override
String toString() => 'AlbumApiModel(userId: $userId, id: $id, title: $title)';

@override
bool operator ==(covariant AlbumApiModel other) {
if (identical(this, other)) return true;

return other.userId == userId && other.id == id && other.title == title;
}

@override
int get hashCode => userId.hashCode ^ id.hashCode ^ title.hashCode;
}

I generate this class with the help of extension in vs code . Or you can also generate this model class for api’s from any website that convert’s the json to dart .

ii ) Api Service / Api Provider :

Make a general class that the general process of fetching the data from the api’s .

import 'dart:convert';

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

abstract class ApiService {
String baseUrl = "https://jsonplaceholder.typicode.com";
String get apiUrl;
String get url => baseUrl + apiUrl;

dynamic fetch({String endpoint = ""}) async {
var response = await http.get(Uri.parse(url + endpoint));
if (response.statusCode == 200) {
return jsonDecode(response.body);
}
return null;
}
}

This class contain’s the url of the api’s and the fetch method that is used to fetch the data from the api’s . (In the same way you can also make the function for all of the possible crud operation’s . In my case i will only need the data to be fetched so i will make only fetch data method ) .

the above code code (class) is valid for all of the api’s of the json place holder . You have to only change the generalize class .

Now you have to make a specialized class for fetching the data from the api’s .

import 'package:flutter_provider_practice_f12/Json_place_holder_api/album_api/album_api_model.dart';
import 'package:flutter_provider_practice_f12/Json_place_holder_api/album_api/api_service.dart';

class AlbumApiService extends ApiService {
static AlbumApiService? _albumApiService;
AlbumApiService._internal();

factory AlbumApiService() {
return _albumApiService ??= AlbumApiService._internal();
}

@override
String get apiUrl => "/albums";

Future<List<AlbumApiModel>> fetchAlbums() async {
List albums = await fetch();
return albums.map((map) => AlbumApiModel.fromMap(map)).toList();
}
}

This class contain’s the specialized url of the api , The singleton design pattern for make a single object for the life time and the specialized fetch method that depend’s upon what type of the data you have in the api’s .

That’s all for the process of fetching the api’s of json place holder . So now we have to add the provider in it .

The process of provider is same for all of the uses . Now i will only explain the changes in the counter and the api’s code .

import 'package:flutter/material.dart';
import 'package:flutter_provider_practice_f12/Json_place_holder_api/album_api/album_api_model.dart';
import 'package:flutter_provider_practice_f12/Json_place_holder_api/album_api/album_api_service.dart';

class AlbumNotifier extends ChangeNotifier {
final AlbumApiService _albumApiService = AlbumApiService();
List<AlbumApiModel>? albumModelList;
bool isInitial = true;
bool isLoading = false;
bool isLoaded = false;

Future<void> fetchAlbums() async {
isInitial = false;
isLoading = true;
isLoaded = false;
notifyListeners();

albumModelList = await _albumApiService.fetchAlbums();
isInitial = false;
isLoading = false;
isLoaded = true;
notifyListeners();
}
}

Now we have to make a custom change notifier class which i named of album notifier class .

now we have to make the final constructor of the Specialized album api service class for fetching the data of the api’s .

We know one thing that our data in api’s is in the form of the list so we have to make a list of model .

In the User Interface we have only four possibilities

Before we load the data an initial screen that consist of the button for fetching the data .

Second when the user click’s on that button then the api’s data is loading and here we will show the circular progress indicator .

If our data from the api’s will be fetched successfully then we will show the data on the screen otherwise wee will show the error page that will show that the data is not found text .

For that purpose i have to make the three variables

bool isInitial = true;
bool isLoading = false;
bool isLoaded = false;

As we know that when the page will be displayed for the first time the initial screen will be displayed so i have to set this value to true . So that our initial state is true when the user navigate on that page . Other two properties are set to false .

import 'package:flutter/material.dart';
import 'package:flutter_provider_practice_f12/Json_place_holder_api/album_api/album_api_model.dart';
import 'package:flutter_provider_practice_f12/Json_place_holder_api/album_api/album_api_service.dart';

class AlbumNotifier extends ChangeNotifier {
final AlbumApiService _albumApiService = AlbumApiService();
List<AlbumApiModel>? albumModelList;
bool isInitial = true;
bool isLoading = false;
bool isLoaded = false;

Future<void> fetchAlbums() async {
isInitial = false;
isLoading = true;
isLoaded = false;
notifyListeners();

try {
albumModelList = await _albumApiService.fetchAlbums();
isInitial = false;
isLoading = false;
isLoaded = true;
notifyListeners();
} catch (e) {
print(e);
}
}
}

After that we have to make a function of fetch data . This is called when the user click’s on the floating action button and the user is trying to fetch the data .

Now our state is change from the initial to the loading state .

after that we will call the notify listener that will notify it’s observer’s when there is some change in the data .

Now we have to wait for the data to be fetched . After the data is fetched successfully then we have to change the state to the loaded state . That’s all for the album notifier class .

Now in the home property or at the top of the widget tree we have to add the change notifier provider .

ChangeNotifierProvider(
create: (context) => AlbumNotifier(),
child: const MyHomePage(title: 'Flutter Demo Home Page'),
)

Now i will use the consumer widget that i have discussed above in the case of the counter .


body: Consumer<AlbumNotifier>(
builder: (context, value, child) {
if (value.isInitial) {
return const AlbumInitialWidget();
} else if (value.isLoading) {
return const AlbumLoadingWidget();
} else {
return const AlbumLoadedWidget();
}
},
),

Now of the bases of the state that i made in the album notifier class i will return the specialized widget’s that i will make in the separate file .

Initial Widget that will displayed a button on the screen to fetch the data .

import 'package:flutter/material.dart';
import 'package:flutter_provider_practice_f12/Json_place_holder_api/album_api/album_notifier.dart';
import 'package:provider/provider.dart';

class AlbumInitialWidget extends StatefulWidget {
const AlbumInitialWidget({super.key});

@override
State<AlbumInitialWidget> createState() => _AlbumInitialWidgetState();
}

class _AlbumInitialWidgetState extends State<AlbumInitialWidget> {
@override
Widget build(BuildContext context) {
return Center(
child: InkWell(
onTap: () {
context.read<AlbumNotifier>().fetchAlbums();
},
child: Container(
width: 300,
height: 70,
alignment: Alignment.center,
color: Colors.amber,
child: Text("Load Albums"),
),
),
);
}
}

Loading screen that will show the circular progress indicator at the center of the screen .

import 'package:flutter/material.dart';

class AlbumLoadingWidget extends StatefulWidget {
const AlbumLoadingWidget({super.key});

@override
State<AlbumLoadingWidget> createState() => _AlbumLoadingWidgetState();
}

class _AlbumLoadingWidgetState extends State<AlbumLoadingWidget> {
@override
Widget build(BuildContext context) {
return const Center(child: CircularProgressIndicator());
}
}

Loaded screen that will show the data of the api’s in the form of the list .

import 'package:flutter/material.dart';
import 'package:flutter_provider_practice_f12/Json_place_holder_api/album_api/album_notifier.dart';
import 'package:provider/provider.dart';

class AlbumLoadedWidget extends StatefulWidget {
const AlbumLoadedWidget({super.key});

@override
State<AlbumLoadedWidget> createState() => _AlbumLoadedWidgetState();
}

class _AlbumLoadedWidgetState extends State<AlbumLoadedWidget> {
@override
Widget build(BuildContext context) {
var albums = context.read<AlbumNotifier>().albumModelList;
return ListView.builder(
itemBuilder: (context, index) => ListTile(
leading: CircleAvatar(
child: Text(albums[index].id.toString()),
),
title: Text(albums[index].title),
trailing: Text(albums[index].userId.toString()),
),
itemCount: albums!.length,
);
}
}

Here i use the list view . If you don’t know about the list view kindly read my article on the list view or you may also get a lot of knowledge from the official documentation .


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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),

home: ChangeNotifierProvider(
create: (context) => AlbumNotifier(),
child: const MyHomePage(title: 'Flutter Demo Home Page')),


);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

void _fetchData() {
Provider.of<AlbumNotifier>(context, listen: false).fetchAlbums();
}

@override
Widget build(BuildContext context) {

return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Consumer<AlbumNotifier>(
builder: (context, value, child) {
if (value.isInitial) {
return const AlbumInitialWidget();
} else if (value.isLoading) {
return const AlbumLoadingWidget();
} else {
return const AlbumLoadedWidget();
}
},
),
floatingActionButton: ButtonBar(
children: [
FloatingActionButton(
onPressed: _fetchData,
tooltip: 'Fetch',
child: const Icon(Icons.refresh),
),
],
),


);
}
}

That’s all for the provider . In the next article i will provide a detailed information on drawback’s of provider and why we need the riverpod .

Hope you have learned a lot of new things 📖

Thanks for reading 😊

--

--