Flutter State Management: Introduction to Provider | (Ch. 17)
Welcome to Chapter 17!
In the previous chapter, we learned about the nightmare of "Prop Drilling." We saw how passing data down through 10 layers of widgets makes your code messy, rigid, and hard to maintain.
Today, we fix it.
We are going to learn Provider. Provider is a wrapper around Flutter's internal tools that makes State Management easy. It allows you to place your data at the very top of your app (like a radio tower) and let any widget, anywhere in the app, "tune in" to listen to that data.
📖 Chapter 17: Table of Contents
- The Mental Model: Radio Station vs. Listeners
- The Three Pillars of Provider
- Step 1: Installation
- Step 2: Create the Data Class (ChangeNotifier)
- Step 3: Provide the Data (ChangeNotifierProvider)
- Step 4: Consume the Data (Provider.of)
- Full Code Example: The Refactored Counter
- FAQ: Frequently Asked Questions
1. The Mental Model: Radio Station vs. Listeners
Forget about code for a second. Imagine a Radio Station.
- The Station: They play music. When the song changes, they broadcast a signal.
- The Atmosphere: The air carries the signal everywhere.
- The Radio: Your car radio tunes in. When the signal changes, your speakers play the new song.
Provider works exactly like this:
- ChangeNotifier (The Station): Holds your data (e.g., User Score). When data changes, it yells "Update!".
- ChangeNotifierProvider (The Atmosphere): Sits at the top of your widget tree and makes the data available to everyone below it.
- Consumer (The Radio): A widget deep in the tree that listens. When the data updates, this widget rebuilds.
2. The Three Pillars of Provider
To implement Provider, you only need to understand these three classes.
-
ChangeNotifier: A built-in Flutter class. It provides a method callednotifyListeners(). You extend this class to create your data model. -
ChangeNotifierProvider: A widget that provides an instance of a ChangeNotifier to its descendants. It comes from the `provider` package. -
Consumer(or `Provider.of`): The way you access the data.
Step 1: Installation
First, add the package to your `pubspec.yaml` file.
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0 # Check pub.dev for the latest version
Run flutter pub get.
Step 2: Create the Data Class (ChangeNotifier)
Instead of putting variables inside a widget, we create a separate class for them. This separates our Logic from our UI.
Let's stick with the classic "Counter" example to keep it simple.
import 'package:flutter/material.dart';
class CounterModel extends ChangeNotifier {
// 1. The State (Private)
int _count = 0;
// 2. Getter (Public access)
int get count => _count;
// 3. Method to modify state
void increment() {
_count++;
// 4. THE MAGIC: Tell everyone listening to update!
notifyListeners();
}
}
When notifyListeners() is called, any widget watching this class will rebuild automatically.
Step 3: Provide the Data (ChangeNotifierProvider)
Now we need to inject this class into the app. Usually, we do this at the very top, in `main.dart`.
import 'package:provider/provider.dart';
void main() {
runApp(
// WRAP your app in ChangeNotifierProvider
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
),
);
}
Now, `CounterModel` is available to `MyApp` and every single widget inside it.
Step 4: Consume the Data (Provider.of)
Now, let's go deep into the widget tree and use the data. We don't need to pass variables through constructors anymore!
We can access the data using Provider.of<T>(context).
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1. LISTEN to the data
// This variable 'counter' is now connected to the model.
// Whenever notifyListeners() is called, this build method runs again.
final counterModel = Provider.of<CounterModel>(context);
return Scaffold(
appBar: AppBar(title: Text("Provider Demo")),
body: Center(
// 2. Display the data
child: Text(
'Count: ${counterModel.count}',
style: TextStyle(fontSize: 40),
),
),
floatingActionButton: FloatingActionButton(
// 3. Call the method
onPressed: () {
counterModel.increment();
},
child: Icon(Icons.add),
),
);
}
}
Full Code Example: The Refactored Counter
Here is the complete `main.dart` file. Notice that `CounterScreen` is a StatelessWidget! We don't need `StatefulWidget` anymore because the state lives outside the UI.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// --- 1. THE LOGIC (Radio Station) ---
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Broadcast the change
}
void reset() {
_count = 0;
notifyListeners();
}
}
// --- 2. THE SETUP (Atmosphere) ---
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const CounterScreen(),
);
}
}
// --- 3. THE UI (Radio Receiver) ---
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
// Access the state
final counter = Provider.of<CounterModel>(context);
return Scaffold(
appBar: AppBar(title: const Text('Provider Basic')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'${counter.count}', // Read the value
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: counter.reset,
child: const Text('Reset'),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: counter.increment, // Call the method
child: const Icon(Icons.add),
),
);
}
}
Why is this better?
- Separation of Concerns: Your logic is in `CounterModel`. Your UI is in `CounterScreen`. The UI code is clean.
- No Prop Drilling: If `CounterScreen` had a child widget, that child could also call `Provider.of` directly without the parent passing anything down.
- Global Access: You can access this count from *any* screen in the app.
❓ FAQ: Frequently Asked Questions
context.watch<T>(): Same as `Provider.of(context)`. It listens for changes and rebuilds the widget. Use this in `build()` to display data.context.read<T>(): Same as `Provider.of(context, listen: false)`. It fetches the object without listening for updates. Use this in functions like `onPressed` to trigger actions (like increment) to avoid unnecessary rebuilds.
MultiProvider in your main function to list as many providers as you want (e.g., UserProvider, CartProvider, ThemeProvider).
Conclusion
You have taken your first step into professional Flutter development.
By moving state out of the widgets and into a `ChangeNotifier`, you have decoupled your code. You can now build complex apps where the "Shopping Cart" icon in the top right knows exactly when you clicked "Add to Cart" on the bottom left.
In the next chapter, we will apply this knowledge to build a real-world example: A Shopper App where we can add items to a cart and see the total cost update instantly across multiple screens.

Comments
Post a Comment