Provider is an excellent package for state management in your Flutter apps. It can sometimes be a bit confusing for beginners, but don't worry! In this article, I will guide you through the process of understanding and using Provider in your projects. Let's dive in!
Note: You can view the whole source code used in this article here.
You might be wondering why you should learn Provider when there are other state management solutions available. The reason is simple: Provider is easy to learn, efficient, and widely adopted by the Flutter community. It helps you manage the state of your app in a clean and organized way, which in turn makes your code more maintainable and scalable.
Before we begin, let's add the Provider package to our Flutter app. Run the command flutter pub add provider
. This will add Provider to your pubspec.yaml
file.
Next, let's import the Provider package by adding the following line at the top of your main.dart
file:
import 'package:provider/provider.dart';
Provider works by exposing an object (usually a model or a view model) to the widget tree. This object can be accessed by any descendant widget that listens to it. When the object changes, the listening widgets will rebuild, allowing your UI to update accordingly.
To use Provider, you need to wrap your widget tree with a ChangeNotifierProvider
. This will ensure that the object is accessible to all its descendants. Then, you can create a class that extends ChangeNotifier
and notify listeners when the state changes.
Let's start with a basic example: a counter app that increases or decreases a number using Provider.
First, create a class called Counter
that extends ChangeNotifier
:
class Counter extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
_count--;
notifyListeners();
}
}
In this class, we have a private variable _count
that stores the current count. We also have two methods, increment
and decrement
, which modify the count and notify listeners.
Now, let's create the UI:
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Provider Example')),
body: Center(child: CountDisplay()),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
Provider.of<Counter>(context, listen: false).increment();
},
child: Icon(Icons.add),
),
SizedBox(height: 10),
FloatingActionButton(
onPressed: () {
Provider.of<Counter>(context, listen: false).decrement();
},
child: Icon(Icons.remove),
),
],
),
),
);
}
}
class CountDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = Provider.of<Counter>(context);
return Text('Count: ${counter.count}', style: TextStyle(fontSize: 24));
}
}
In our main
function, we wrap the MyApp
widget with a ChangeNotifierProvider
, which provides an instance of the Counter
class to the widget tree. Inside the MyApp
widget, we have a Scaffold
with a CountDisplay
widget at the center and two FloatingActionButton
widgets for incrementing and decrementing the count.
In the CountDisplay
widget, we use Provider.of<Counter>(context)
to access the Counter
instance. This will ensure that the widget rebuilds whenever the count changes. We then display the current count using a Text
widget.
In the two FloatingActionButton
widgets, we use the increment
and decrement
methods from the Counter
class to modify the count. Notice that we pass listen: false
when accessing the Counter
instance in these buttons. This is because we don't want the buttons to rebuild when the count changes.
Now that you understand the basics of Provider, let's move on to a more complex example: a To-Do List app. In this app, we'll have a list of tasks that can be marked as complete or incomplete.
First, create a Task
class to represent a single task:
class Task {
final String title;
bool isComplete;
Task({required this.title, this.isComplete = false});
}
Next, create a TaskList
class that extends ChangeNotifier
:
class TaskList extends ChangeNotifier {
List<Task> _tasks = [];
List<Task> get tasks => _tasks;
void addTask(Task task) {
_tasks.add(task);
notifyListeners();
}
void toggleTask(Task task) {
task.isComplete = !task.isComplete;
notifyListeners();
}
void removeTask(Task task) {
_tasks.remove(task);
notifyListeners();
}
}
This class maintains a list of tasks and provides methods to add, toggle, and remove tasks. Don't forget to call notifyListeners()
whenever the list changes.
Now, let's create the UI for our To-Do List app:
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => TaskList(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('To-Do List')),
body: TaskListView(),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final taskTitle = await showDialog<String>(
context: context,
builder: (context) => AddTaskDialog(),
);
if (taskTitle != null) {
Provider.of<TaskList>(context, listen: false).addTask(Task(title: taskTitle));
}
},
child: Icon(Icons.add),
),
),
);
}
}
class TaskListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final taskList = Provider.of<TaskList>(context);
return ListView.builder(
itemCount: taskList.tasks.length,
itemBuilder: (context, index) {
final task = taskList.tasks[index];
return ListTile(
title: Text(task.title),
leading: Checkbox(
value: task.isComplete,
onChanged: (newValue) {
taskList.toggleTask(task);
},
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
taskList.removeTask(task);
},
),
);
},
);
}
}
class AddTaskDialog extends StatefulWidget {
@override
_AddTaskDialogState createState() => _AddTaskDialogState();
}
class _
_AddTaskDialogState createState() => _AddTaskDialogState();
}
class _AddTaskDialogState extends State<AddTaskDialog> {
TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Add Task'),
content: TextField(
controller: _controller,
decoration: InputDecoration(labelText: 'Task title'),
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
),
TextButton(
onPressed: () {
if (_controller.text.isNotEmpty) {
Navigator.of(context).pop(_controller.text);
}
},
child: Text('Add'),
),
],
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
In the main
function, we wrap our MyApp
widget with a ChangeNotifierProvider
that provides an instance of the TaskList
class to the widget tree. Inside the MyApp
widget, we have a Scaffold
with a TaskListView
widget and a FloatingActionButton
for adding tasks.
In the TaskListView
widget, we use Provider.of<TaskList>(context)
to access the TaskList
instance and display the tasks using a ListView.builder
. We also use the toggleTask
and removeTask
methods to update the tasks' state and remove them from the list.
The AddTaskDialog
widget is a simple dialog that prompts the user to enter a task title. Once the user adds a task, the title is passed back to the MyApp
widget, which then adds the new task to the TaskList
.
In this chapter, we will delve deeper into the Provider package and explore its advanced features, including FutureProvider
, StreamProvider
, Provider
, and MultiProvider
. These tools will help you better manage the state of your Flutter applications and deal with asynchronous data more effectively.
FutureProvider
is a useful widget for providing a value that is obtained through a Future
. It will automatically handle the loading state while waiting for the Future
to complete and update the UI accordingly.
Let's create an example where we fetch a list of random numbers from a server:
class NumberList {
Future<List<int>> fetchNumbers() async {
await Future.delayed(Duration(seconds: 2));
return List.generate(10, (index) => Random().nextInt(100));
}
}
To use FutureProvider
, wrap your widget tree with it and provide an instance of NumberList
:
FutureProvider<List<int>>(
create: (context) => NumberList().fetchNumbers(),
child: MyApp(),
),
Inside the MyApp
widget, you can access the Future
value with a Consumer
widget:
Consumer<List<int>>(
builder: (context, numbers, child) {
if (numbers == null) {
return CircularProgressIndicator();
} else {
return ListView.builder(
itemCount: numbers.length,
itemBuilder: (context, index) => ListTile(title: Text('${numbers[index]}')),
);
}
},
),
StreamProvider
is similar to FutureProvider
, but it works with Stream
objects instead of Future
. This is especially useful when dealing with real-time data updates. Let's modify the previous example to use a Stream
that emits random numbers:
class NumberStream {
Stream<int> randomNumbers() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield Random().nextInt(100);
}
}
}
Now, replace FutureProvider
with StreamProvider
:
StreamProvider<List<int>>(
create: (context) => NumberStream().randomNumbers(),
initialData: [],
child: MyApp(),
),
And update the MyApp
widget to consume the stream:
Consumer<int>(
builder: (context, number, child) {
return ListTile(title: Text('$number'));
},
),
Provider
is a more generic implementation that allows you to provide any value without handling Future
or Stream
. This can be useful when you want to provide simple values, like configurations or other constant data.
For example, providing a theme configuration:
class ThemeConfig {
final String fontFamily;
final Color primaryColor;
ThemeConfig({required this.fontFamily, required this.primaryColor});
}
final themeConfig = ThemeConfig(fontFamily: 'Roboto', primaryColor: Colors.blue);
Provider<ThemeConfig>(
create: (context) => themeConfig,
child: MyApp(),
),
In the MyApp
widget, you can access the ThemeConfig
instance:
final themeConfig = Provider.of<ThemeConfig>(context);
When your app requires multiple providers, it can be cumbersome to nest them manually. MultiProvider
is a convenient widget that allows you to provide multiple providers in a more concise manner.
Assuming we want to provide both the NumberList
and ThemeConfig
from the previous examples, we can use MultiProvider
:
MultiProvider(
providers: [
FutureProvider<List<int>>(
create: (context) => NumberList().fetchNumbers(),
),
Provider<ThemeConfig>(
create: (context) => themeConfig,
),
],
child: MyApp(),
),
Now, both providers are available within the MyApp
widget tree, and you can use them as needed:
Consumer2<List<int>, ThemeConfig>(
builder: (context, numbers, themeConfig, child) {
if (numbers == null) {
return CircularProgressIndicator();
} else {
return ListView.builder(
itemCount: numbers.length,
itemBuilder: (context, index) => ListTile(
title: Text(
'${numbers[index]}',
style: TextStyle(fontFamily: themeConfig.fontFamily),
),
),
);
}
},
),
In this article, you've learned the basics of state management using the Provider package in Flutter. We've covered how to create a simple counter app and a more complex To-Do List app. Provider is an excellent choice for managing the state of your Flutter apps, as it's easy to learn, efficient, and widely adopted by the community.
If you want to learn more about Provider and other state management solutions, check out the official Flutter documentation.
If you enjoyed this tutorial and found it helpful, please give it some claps! And don't forget to follow me for more quality content on Flutter and other topics.}