Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, offers a rich set of tools for state management. Among these tools, the provider
package stands out as a simple, flexible, and widely adopted solution. This article provides an in-depth examination of the provider
package, its core concepts, implementation, and best practices.
What is the Provider Package?
The provider
package is a wrapper around InheritedWidget
, making it more accessible and usable for managing application state in Flutter. It is designed to simplify state management, reduce boilerplate code, and improve the overall structure and maintainability of your Flutter applications.
Why Use Provider?
- Simplicity: Simplifies the process of state management, reducing the complexity compared to other approaches.
- Centralized State: Provides a centralized way to manage and access application state, making it easier to maintain and debug.
- Flexibility: Supports various types of state, including simple variables, complex objects, and even BLoC patterns.
- Accessibility: Makes state easily accessible throughout the widget tree, allowing any widget to read or modify the state.
- Reduces Boilerplate: Reduces the amount of boilerplate code needed to manage state compared to traditional
InheritedWidget
usage.
Core Concepts
The provider
package revolves around a few core concepts:
- Providers: Widgets that provide a value (state) to their descendants. Examples include
Provider
,ChangeNotifierProvider
,StreamProvider
, andFutureProvider
. - Consumers: Widgets that listen to changes in the provided value and rebuild when the value changes. The most common consumer is the
Consumer
widget. - ChangeNotifier: A class from the
flutter:foundation
library that allows widgets to listen for changes. Used in conjunction withChangeNotifierProvider
to manage simple state.
Implementing Provider in Flutter
Let’s dive into how to implement provider
in your Flutter application with code examples.
Step 1: Add Dependency
Add the provider
package to your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0 # Use the latest version
Run flutter pub get
to install the package.
Step 2: Create a ChangeNotifier Class
ChangeNotifier
is a class that provides change notifications to its listeners. It’s used for simple state management.
import 'package:flutter/foundation.dart';
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Notify listeners about the change
}
}
Step 3: Provide the ChangeNotifier
Use ChangeNotifierProvider
to provide an instance of your ChangeNotifier
to the widget tree.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
Step 4: Consume the Provided Value
Use Consumer
to access the provided value and rebuild the widget when the value changes.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Provider Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Consumer<Counter>(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: Theme.of(context).textTheme.headlineMedium,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Provider.of<Counter>(context, listen: false).increment();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Explanation:
Provider.of<Counter>(context, listen: false)
retrieves theCounter
instance from the context.listen: false
is used because we only want to trigger the increment function and not rebuild the widget.- The
Consumer<Counter>
widget listens for changes to theCounter
instance. WhennotifyListeners()
is called in theCounter
class, theConsumer
rebuilds its child with the new value.
Advanced Usage
The provider
package supports various advanced scenarios, including:
- Multiple Providers: Using
MultiProvider
to provide multiple values. - StreamProvider: Listening to a
Stream
and providing its latest value. - FutureProvider: Listening to a
Future
and providing its resolved value. - ProxyProvider: Combining multiple providers to create a derived value.
Multiple Providers
To provide multiple values, use MultiProvider
:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Value1 with ChangeNotifier {
int value = 0;
void increment() {
value++;
notifyListeners();
}
}
class Value2 with ChangeNotifier {
String message = "Hello";
void updateMessage(String newMessage) {
message = newMessage;
notifyListeners();
}
}
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => Value1()),
ChangeNotifierProvider(create: (context) => Value2()),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("MultiProvider Example")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<Value1>(
builder: (context, value1, child) => Text('Value 1: ${value1.value}'),
),
Consumer<Value2>(
builder: (context, value2, child) => Text('Value 2: ${value2.message}'),
),
ElevatedButton(
onPressed: () {
Provider.of<Value1>(context, listen: false).increment();
},
child: Text('Increment Value 1'),
),
ElevatedButton(
onPressed: () {
Provider.of<Value2>(context, listen: false).updateMessage("New Message");
},
child: Text('Update Value 2'),
),
],
),
),
),
);
}
}
StreamProvider
To listen to a Stream
and provide its latest value:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Stream<int> numberStream() {
return Stream.periodic(Duration(seconds: 1), (count) => count);
}
void main() {
runApp(
MaterialApp(
home: StreamProvider<int>(
create: (_) => numberStream(),
initialData: 0,
child: MyApp(),
),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("StreamProvider Example")),
body: Center(
child: Consumer<int>(
builder: (context, number, child) => Text('Stream Value: $number'),
),
),
);
}
}
FutureProvider
To listen to a Future
and provide its resolved value:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Future<String> fetchMessage() async {
await Future.delayed(Duration(seconds: 2));
return "Data Fetched!";
}
void main() {
runApp(
MaterialApp(
home: FutureProvider<String>(
create: (_) => fetchMessage(),
initialData: "Loading...",
child: MyApp(),
),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("FutureProvider Example")),
body: Center(
child: Consumer<String>(
builder: (context, message, child) => Text('Future Value: $message'),
),
),
);
}
}
ProxyProvider
To combine multiple providers and create a derived value:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Value1 with ChangeNotifier {
int value = 0;
void increment() {
value++;
notifyListeners();
}
}
class Value2 with ChangeNotifier {
int factor = 2;
void setFactor(int newFactor) {
factor = newFactor;
notifyListeners();
}
}
class CombinedValue {
final int value1;
final int value2;
CombinedValue(this.value1, this.value2);
int get multipliedValue => value1 * value2;
}
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => Value1()),
ChangeNotifierProvider(create: (context) => Value2()),
ProxyProvider2<Value1, Value2, CombinedValue>(
update: (context, value1, value2, previous) =>
CombinedValue(value1.value, value2.factor),
),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("ProxyProvider Example")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<CombinedValue>(
builder: (context, combinedValue, child) =>
Text('Multiplied Value: ${combinedValue.multipliedValue}'),
),
ElevatedButton(
onPressed: () {
Provider.of<Value1>(context, listen: false).increment();
},
child: Text('Increment Value 1'),
),
ElevatedButton(
onPressed: () {
Provider.of<Value2>(context, listen: false).setFactor(3);
},
child: Text('Set Factor to 3'),
),
],
),
),
),
);
}
}
Best Practices
When working with the provider
package, consider the following best practices:
- Keep State Simple: Use
ChangeNotifier
for simple state. For complex state, consider more robust solutions like BLoC or Redux. - Avoid Deep Nesting: Deeply nested
Consumer
widgets can make the code harder to read and maintain. - Use
select
: To only rebuild when a specific part of the value changes, use theselect
method inConsumer
. - Use
Provider.of<T>(context, listen: false)
Correctly: Only uselisten: false
when you do not need to rebuild the widget on value changes. - Dispose Resources: Properly dispose of resources when using
ChangeNotifier
to prevent memory leaks.
Using select
To rebuild only when a specific part of the value changes:
Consumer<Counter>(
builder: (context, counter, child) {
return Text('Count: ${counter.count}');
},
select: (Counter counter) => counter.count,
)
Disposing Resources
To properly dispose of resources in ChangeNotifier
, override the dispose
method:
class MyModel with ChangeNotifier {
// Resources that need to be disposed
@override
void dispose() {
// Dispose resources here
super.dispose();
}
}
Conclusion
The provider
package is a powerful and versatile tool for state management in Flutter. Its simplicity and flexibility make it an excellent choice for a wide range of applications, from simple to complex. By understanding the core concepts, implementation details, and best practices, you can leverage provider
to build robust and maintainable Flutter applications.