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
InheritedWidgetusage.
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
Consumerwidget. - ChangeNotifier: A class from the
flutter:foundationlibrary that allows widgets to listen for changes. Used in conjunction withChangeNotifierProviderto 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 theCounterinstance from the context.listen: falseis used because we only want to trigger the increment function and not rebuild the widget.- The
Consumer<Counter>widget listens for changes to theCounterinstance. WhennotifyListeners()is called in theCounterclass, theConsumerrebuilds its child with the new value.
Advanced Usage
The provider package supports various advanced scenarios, including:
- Multiple Providers: Using
MultiProviderto provide multiple values. - StreamProvider: Listening to a
Streamand providing its latest value. - FutureProvider: Listening to a
Futureand 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
ChangeNotifierfor simple state. For complex state, consider more robust solutions like BLoC or Redux. - Avoid Deep Nesting: Deeply nested
Consumerwidgets can make the code harder to read and maintain. - Use
select: To only rebuild when a specific part of the value changes, use theselectmethod inConsumer. - Use
Provider.of<T>(context, listen: false)Correctly: Only uselisten: falsewhen you do not need to rebuild the widget on value changes. - Dispose Resources: Properly dispose of resources when using
ChangeNotifierto 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.