State Management in Flutter with Provider

Ayşe ALMACI
6 min readNov 19, 2024

--

Hello everyone! In this article, I want to talk about Provider which is one of the most popular packages of Flutter.

What is Provider?

Provider is essentially a wrapper around InheritedWidget. It facilitates data sharing and state updates between widgets. It is easy to use and is a recommended method by the Flutter team.

Advantages of Provider:

  • Ease of Use: Provider has a simple and understandable structure.
  • Performance: Being built on InheritedWidget, it provides a performant solution.
  • Flexibility: Different types of providers cater to various state management needs.
  • Testability: State management becomes more testable with Provider.
  • Community Support: It has a large user base and strong community support.

How To Use?

1.Add the Dependency

add the provider package to your pubspec.yaml file.

dependencies:
flutter:
sdk: flutter
provider: ^6.1.2

Then, run flutter pub get in terminal.

You can look at there: https://pub.dev/packages/provider/install

2. Create a Model

Create the data model that your app will use and extend it with ChangeNotifier. This class will hold your data and the methods to modify it.

class CounterModel with ChangeNotifier {
int _count = 0;

int get count => _count;

void increment() {
_count++;
notifyListeners(); // Notifies widgets of changes
}
}

3. Add the Provider to The Widget Tree:

Use ChangeNotifierProvider (or another provider type as needed) to add your model high in the widget tree.

void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MyApp(),
),
);
}

4. Access and Use The Provider:

In your widgets, use Provider.of<T>(context) to access the model. Set listen to false

if you only need to access data and don't need to listen for changes.

In example below, Consumer rebuilds only the Text widget when the model changes. This improves performance.

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Provider Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Counter Value:',
),
Consumer<CounterModel>( // Rebuilds only the Text widget
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Provider.of<CounterModel>(context, listen: false).increment();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

Provider.of accesses the model and listens for changes. Consumer rebuilds only a specific widget.

Types of Providers:

The Provider package offers several types of providers for different needs:

ChangeNotifierProvider: A provider that listens for changes and updates widgets.

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

// Model class
class CounterModel with ChangeNotifier {
int _count = 0;

int get count => _count;

void increment() {
_count++;
notifyListeners(); // Notify widgets about the change
}
}

void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(), // Create an instance of the model
child: MyApp(),
),
);
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('ChangeNotifierProvider Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<CounterModel>( // Listen for changes
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: TextStyle(fontSize: 48),
);
},
),
ElevatedButton(
onPressed: () {
Provider.of<CounterModel>(context, listen: false).increment(); // Increment the counter
},
child: Text('Increment'),
),
],
),
),
),
);
}
}
  • CounterModel: Holds the counter value (_count) and notifies widgets when it changes.
  • ChangeNotifierProvider: Makes the CounterModel accessible to the widget tree.
  • Consumer<CounterModel>: A widget that listens for changes in CounterModel and rebuilds its child (the Text widget) when the counter updates.
  • ElevatedButton: When pressed, it increases the counter value in CounterModel, triggering the Consumer to rebuild and update the displayed count.

FutureProvider: Used for asynchronous operations.

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

// Asynchronous operation (e.g., fetching data from an API)
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return "Data fetched!";
}

void main() {
runApp(
FutureProvider<String>(
create: (context) => fetchData(), // Start the asynchronous operation
initialData: "Loading data...", // Initial value
child: MyApp(),
),
);
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('FutureProvider Example'),
),
body: Center(
child: Consumer<String>( // Listen for the data
builder: (context, data, child) {
return Text(
data,
style: TextStyle(fontSize: 24),
);
},
),
),
),
);
}
}
  • fetchData(): Simulates an asynchronous operation (e.g., an API call).
  • FutureProvider: Executes fetchData() and provides its result (a String) to the widget tree. It also shows "Loading data..." initially while waiting for the result.
  • Consumer<String>: A widget that listens for the result of the Future and rebuilds its child (the Text widget) to display the fetched data when it's available.

StreamProvider: Used to manage data from streams.

import 'dart:async';

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

// Create a stream
Stream<int> generateStream() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}

void main() {
runApp(
StreamProvider<int>(
create: (context) => generateStream(), // Start the stream
initialData: 0, // Initial value
child: MyApp(),
),
);
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('StreamProvider Example'),
),
body: Center(
child: Consumer<int>( // Listen to the stream
builder: (context, data, child) {
return Text(
'$data',
style: TextStyle(fontSize: 48),
);
},
),
),
),
);
}
}
  • generateStream(): Creates a stream that emits numbers from 1 to 10 with a 1-second delay between each number.
  • StreamProvider: Makes the stream provided by generateStream() available to the widget tree. It also provides an initial value (0) to display before the stream emits any data.
  • Consumer<int>: A widget that listens for new values emitted by the stream and rebuilds its child (the Text widget) to display the latest value.

ValueListenableProvider: Listens for ValueListenable objects. A provider that works with ValueNotifier.

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

void main() {
runApp(
ValueListenableProvider(
create: (context) => ValueNotifier(0), // Create a ValueNotifier
child: MyApp(),
),
);
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('ValueListenableProvider Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<int>( // Listen for the value
builder: (context, value, child) {
return Text(
'$value',
style: TextStyle(fontSize: 48),
);
},
),
ElevatedButton(
onPressed: () {
Provider.of<ValueNotifier<int>>(context, listen: false).value++; // Increment the value
},
child: Text('Increment'),
),
],
),
),
),
);
}
}
  • ValueListenableProvider: Creates a ValueNotifier (a special object that holds a value and notifies listeners when it changes) and makes it accessible to the widget tree.
  • Consumer<int>: A widget that listens for changes to the value held by the ValueNotifier and rebuilds its child (the Text widget) to display the updated value.
  • ElevatedButton: When pressed, it increments the value in the ValueNotifier, triggering the Consumer to rebuild and show the new value.

Provider: The basic provider type, can provide any object.

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

class DataModel {
String data = "Initial data";
}

void main() {
runApp(
Provider<DataModel>(
create: (context) => DataModel(), // Create an instance of DataModel
child: MyApp(),
),
);
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Provider Example'),
),
body: Center(
child: Consumer<DataModel>( // Listen for the data
builder: (context, dataModel, child) {
return Text(
dataModel.data,
style: TextStyle(fontSize: 24),
);
},
),
),
),
);
}
}
  • DataModel: A simple class holding the data (data) to be shared.
  • Provider: Makes an instance of DataModel accessible to the widget tree.
  • Consumer<DataModel>: A widget that retrieves the DataModel instance from the Provider and uses its data to display the text.

MultiProvider

Allows you to add multiple providers to a single widget tree.

void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => ModelA()),
ChangeNotifierProvider(create: (context) => ModelB()),
Provider(create: (context) => ModelC()),
],
child: MyApp(),
),
);
}

ProxyProvider

Creates a new value using values ​​from one or more providers.

Assume that you have a user model and a settings model. In your user model, you store the user’s name, and in your settings model, you store the user’s preferred theme.

Using ProxyProvider, you can combine the information from these two models to create a new model that contains the user’s name and preferred theme.

There are different variations of ProxyProvider (ProxyProvider0, ProxyProvider2, ProxyProvider3, etc.).

These numbers indicate how many providers the ProxyProvider receives values ​​from.

class UserModel with ChangeNotifier {
String _name = 'John Doe';
String get name => _name;
void changeName(String newName) {
_name = newName;
notifyListeners();
}
}

class ThemeModel with ChangeNotifier {
String _theme = 'light';
String get theme => _theme;
void changeTheme(String newTheme) {
_theme = newTheme;
notifyListeners();
}
}

class UserViewModel {
final String name;
final String theme;

UserViewModel({required this.name, required this.theme});
}

void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => UserModel()),
ChangeNotifierProvider(create: (context) => ThemeModel()),
ProxyProvider2<UserModel, ThemeModel, UserViewModel>(
update: (context, userModel, themeModel, previous) =>
UserViewModel(name: userModel.name, theme: themeModel.theme),
),
],
child: MyApp(),
),
);
}

Thank you for taking the time to read my article. If you’d like to get in touch, you can reach me through my LinkedIn and Twitter accounts.

Also, don’t forget to check out my other articles on topics such as cybersecurity, network, artificial intelligence, Flutter on my profile.

See you in the next article!

#flutter #mobildevelopment #provider #statemanagement #android #ios #crossplatform

--

--

Ayşe ALMACI
Ayşe ALMACI

Written by Ayşe ALMACI

Computer Engineer | Flutter Ankara Organizer & Flutter Türkiye Event Organizer