Flutter State Management: Introduction to Provider | (Ch. 17)

Flutter Course Chapter 17: Introduction to Provider, illustrating the "Radio Station" analogy where a central Provider broadcasts state changes to listening widgets across the application.

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.

1. The Mental Model: Radio Station vs. Listeners

Forget about code for a second. Imagine a Radio Station.

  1. The Station: They play music. When the song changes, they broadcast a signal.
  2. The Atmosphere: The air carries the signal everywhere.
  3. The Radio: Your car radio tunes in. When the signal changes, your speakers play the new song.

Provider works exactly like this:

  1. ChangeNotifier (The Station): Holds your data (e.g., User Score). When data changes, it yells "Update!".
  2. ChangeNotifierProvider (The Atmosphere): Sits at the top of your widget tree and makes the data available to everyone below it.
  3. 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 called notifyListeners(). 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?

  1. Separation of Concerns: Your logic is in `CounterModel`. Your UI is in `CounterScreen`. The UI code is clean.
  2. No Prop Drilling: If `CounterScreen` had a child widget, that child could also call `Provider.of` directly without the parent passing anything down.
  3. Global Access: You can access this count from *any* screen in the app.

❓ FAQ: Frequently Asked Questions

Q: I see `context.watch` and `context.read` in other tutorials. What are they?
These are shortcuts (extension methods) provided by the package. They do the same thing but are easier to read:
  • 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.
Q: Can I have multiple Providers?
Yes! You use a widget called MultiProvider in your main function to list as many providers as you want (e.g., UserProvider, CartProvider, ThemeProvider).
Q: Does `notifyListeners()` rebuild the whole app?
No! It only rebuilds the widgets that are specifically listening to that provider. If you have a `SettingsScreen` listening to `ThemeModel`, changing the theme won't rebuild your `ChatScreen` unless the chat is also listening. This makes it very efficient.

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