Flutter Provider Deep Dive: MultiProvider & Performance | (Ch. 18)
Welcome to Chapter 18!
In the last chapter, we introduced Provider. We learned how to wrap our app in a ChangeNotifierProvider and listen to it using Provider.of.
But that was just the "Hello World" of State Management.
Real apps are complex. You will have a User Provider (for login info), a Cart Provider (for shopping), a Settings Provider (for themes), and maybe ten others. You also need to make sure your app doesn't lag because you are rebuilding the whole screen when only a tiny text label changes.
In this deep dive, we will master MultiProvider, learn the crucial difference between Read vs. Watch, and optimize our app using Consumer and Selector.
📖 Chapter 18: Table of Contents
1. The Problem with Nesting Providers
Imagine your app needs three different providers. If you use the method from Chapter 17, your `main.dart` will look like a pyramid of doom:
// ❌ BAD PRACTICE: Nesting Hell
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => AuthProvider(),
child: ChangeNotifierProvider(
create: (context) => CartProvider(),
child: ChangeNotifierProvider(
create: (context) => ThemeProvider(),
child: const MyApp(),
),
),
),
);
}
This is hard to read and hard to edit. As your app grows, this pyramid will grow deeper and deeper.
2. The Solution: `MultiProvider`
The `provider` package gives us a clean way to declare all our state objects in one list. It flattens the pyramid.
// ✅ GOOD PRACTICE: MultiProvider
import 'package:provider/provider.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AuthProvider()),
ChangeNotifierProvider(create: (context) => CartProvider()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
],
child: const MyApp(),
),
);
}
This is much cleaner. `MultiProvider` simply injects all these classes into the widget tree so any child widget can access `AuthProvider`, `CartProvider`, or `ThemeProvider`.
3. Performance: `read` vs `watch`
This is the #1 mistake beginners make. They use `watch` everywhere, causing their app to rebuild unnecessarily.
Context.watch<T>()
- What it does: Gets the data AND subscribes to changes.
- Effect: Whenever `notifyListeners()` is called, the widget that called `.watch` will rebuild.
- Use Case: Inside the `build()` method when you need to display UI data (e.g., Text, Colors).
Context.read<T>()
- What it does: Gets the data (or the class) BUT DOES NOT subscribe.
- Effect: Even if `notifyListeners()` is called, this widget will NOT rebuild.
- Use Case: Inside functions like `onPressed` or `onTap` where you just need to call a method (e.g., `increment()`, `login()`).
// Example of Correct Usage
Widget build(BuildContext context) {
// We WATCH the count because we need to show it
final count = context.watch<CounterModel>().count;
return Scaffold(
body: Text('$count'),
floatingActionButton: FloatingActionButton(
onPressed: () {
// We READ the model because we just want to call a function.
// We don't need to rebuild the button when count changes.
context.read<CounterModel>().increment();
},
),
);
}
4. Optimization 1: The `Consumer` Widget
Sometimes, calling `Provider.of` or `context.watch` at the top of your `build` method is inefficient. It rebuilds the *entire* widget, including parts that didn't change (like the AppBar or background).
The Consumer widget allows you to be surgical. It wraps only the tiny part of the UI that needs to update.
// Inside a huge complex screen...
Scaffold(
appBar: AppBar(title: Text("Huge Screen")), // Does not rebuild
body: Column(
children: [
BigImageHeader(), // Does not rebuild
// ONLY this Text widget rebuilds when count changes
Consumer<CounterModel>(
builder: (context, counter, child) {
return Text("Count: ${counter.count}");
},
),
FooterWidget(), // Does not rebuild
],
),
);
5. Optimization 2: The `Selector` Widget
What if your Provider has Two variables?
String name;
int age;
If you use `Consumer` or `watch`, changing the `age` will also rebuild widgets that are only displaying the `name`. That is wasteful.
The Selector widget is a smarter Consumer. It listens to a specific value, not the whole class.
Selector<UserModel, String>(
// 1. SELECT only the 'name' property
selector: (context, user) => user.name,
// 2. BUILD only when 'name' changes.
// If 'user.age' changes, this builder is IGNORED.
builder: (context, name, child) {
return Text("User: $name");
},
);
This is the ultimate optimization tool for large, complex providers.
6. Organizing Your Code (Best Practices)
As you add more providers, keep your project structure clean:
- /lib/models/ or /lib/providers/: Put your `ChangeNotifier` classes here.
- /lib/screens/: Your UI code.
- /lib/main.dart: Keep this file clean. Only setup `MultiProvider` and `MaterialApp`.
7. Full Example: Settings & User Profile
Let's build a mini-app with two providers: `ThemeModel` (for Dark Mode) and `UserModel` (for Name).
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// --- PROVIDER 1: THEME ---
class ThemeModel extends ChangeNotifier {
bool _isDark = false;
bool get isDark => _isDark;
void toggleTheme() {
_isDark = !_isDark;
notifyListeners();
}
}
// --- PROVIDER 2: USER ---
class UserModel extends ChangeNotifier {
String _name = "Guest";
String get name => _name;
void changeName(String newName) {
_name = newName;
notifyListeners();
}
}
// --- MAIN SETUP ---
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeModel()),
ChangeNotifierProvider(create: (_) => UserModel()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Watch the theme to update the whole app colors
final isDark = context.watch<ThemeModel>().isDark;
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: isDark ? ThemeData.dark() : ThemeData.light(),
home: const SettingsScreen(),
);
}
}
// --- THE SCREEN ---
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
print("Screen Rebuilds"); // Debug print
return Scaffold(
appBar: AppBar(title: const Text("MultiProvider Demo")),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Consumer for User Name
Consumer<UserModel>(
builder: (context, user, child) {
return Text("Current User: ${user.name}",
style: const TextStyle(fontSize: 24));
},
),
const SizedBox(height: 20),
TextField(
decoration: const InputDecoration(labelText: "Enter Name"),
onSubmitted: (value) {
// READ (Action)
context.read<UserModel>().changeName(value);
},
),
const Divider(height: 40),
// Consumer for Theme Toggle
Consumer<ThemeModel>(
builder: (context, theme, child) {
return SwitchListTile(
title: const Text("Dark Mode"),
value: theme.isDark,
onChanged: (val) {
// READ (Action) inside a Consumer is also fine via 'theme'
theme.toggleTheme();
},
);
},
),
],
),
),
);
}
}
❓ FAQ: Frequently Asked Questions
However, Provider is still the most widely used package in existing company codebases. We teach Provider first because you are more likely to encounter it in a job interview. Once you master Provider, learning Riverpod is easy.
ProxyProvider. For example, if your `CartProvider` needs to know who the current user is from `AuthProvider`, you can use ProxyProvider to inject one into the other. This is an advanced topic, but know that it is possible!
Conclusion
You are no longer a beginner. You now understand how to structure a scalable Flutter application.
By using MultiProvider, you can manage dozens of data sources. By understanding read vs watch and using Consumer, you ensure your app runs at a smooth 60 Frames Per Second, even on older phones.
In the next chapter, we will move away from code logic and talk about Data Persistence. How do we save this data so it doesn't disappear when we close the app? We will learn about Shared Preferences.
Don't forget to check the full curriculum here: The Complete Flutter Course.

Comments
Post a Comment