52  Provider (Counter App)

I’ll show you how to implement the same counter app using Provider, which is built on top of InheritedWidget but provides a much cleaner API.

52.1 Provider Solution

First, you’ll need to add Provider to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.1

52.1.1 Counter Model (Business Logic)

import 'package:flutter/foundation.dart';

class CounterModel extends ChangeNotifier {
  int _counter = 0;

  int get counter => _counter;

  void increment() {
    _counter++;
    notifyListeners(); // Tells Provider to rebuild dependent widgets
  }
}

52.1.2 Main App Implementation

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => CounterModel(),
      child: MaterialApp(
        home: HomePage(),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Clean - no props needed!
    return Scaffold(
      appBar: AppBar(title: Text('Provider Counter')),
      body: MainContent(),
    );
  }
}

class MainContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Clean - no props needed!
    return Center(
      child: CounterSection(),
    );
  }
}

class CounterSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        CounterDisplay(),
        SizedBox(height: 20),
        CounterButton(),
      ],
    );
  }
}

class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Option 1: Watch for changes (rebuilds when counter changes)
    final counter = context.watch<CounterModel>().counter;
    
    return Text(
      'Count: $counter',
      style: Theme.of(context).textTheme.headlineMedium,
    );
  }
}

class CounterButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      // Option 2: Read without watching (doesn't rebuild this widget)
      onPressed: () => context.read<CounterModel>().increment(),
      child: Text('Increment'),
    );
  }
}

52.2 Widget Tree Diagram with Provider

MyApp
└── ChangeNotifierProvider<CounterModel>
    ├── Creates and manages: CounterModel instance
    ├── Provides to all descendants
    └── MaterialApp
        └── HomePage (clean - no props!)
            └── MainContent (clean - no props!)
                └── CounterSection (clean - no props!)
                    ├── CounterDisplay
                    │   └── context.watch<CounterModel>() ──┐
                    │       (rebuilds on changes)          │
                    └── CounterButton                       │
                        └── context.read<CounterModel>() ───┤
                            (calls methods, no rebuild)     │
                                                           │
┌──────────────────────────────────────────────────────────┘
│
▼
CounterModel (Business Logic)
├── _counter: int
├── get counter → int
├── increment() → notifyListeners()
└── extends ChangeNotifier

52.3 Data Flow and Callback Diagram

Provider System Flow:

┌─────────────────────────────────────────┐
│         ChangeNotifierProvider          │
│  ┌─────────────────────────────────┐    │
│  │        CounterModel             │    │
│  │  ┌─────────────────────────┐    │    │
│  │  │ _counter: 0             │    │    │
│  │  │ increment() {           │    │    │
│  │  │   _counter++;           │    │    │
│  │  │   notifyListeners();    │    │    │
│  │  │ }                       │    │    │
│  │  └─────────────────────────┘    │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘
                    │
                    │ Provides model to all descendants
                    ▼
        ┌───────────────────────────────┐
        │       Widget Tree             │
        │                               │
        │  HomePage                     │
        │    └── MainContent            │
        │          └── CounterSection   │
        │                ├── CounterDisplay ◄── context.watch<CounterModel>()
        │                └── CounterButton  ◄── context.read<CounterModel>()
        └───────────────────────────────┘

Interaction Flow:

1. User Taps Button
        │
        ▼
2. context.read<CounterModel>().increment()
        │
        ▼
3. CounterModel._counter++
        │
        ▼
4. notifyListeners() called
        │
        ▼
5. Provider notifies all listeners
        │
        ▼
6. Only CounterDisplay rebuilds (because it uses context.watch)
        │
        ▼
7. UI updates with new counter value

Watch vs Read:
┌─────────────────┬─────────────────┐
│ context.watch   │ context.read    │
├─────────────────┼─────────────────┤
│ • Rebuilds      │ • No rebuild    │
│ • Use for UI    │ • Use for       │
│   that shows    │   callbacks/    │
│   data          │   methods       │
│ • Like          │ • Like calling  │
│   subscribing   │   a function    │
└─────────────────┴─────────────────┘

52.4 Key Provider Concepts

52.4.1 1. Separation of Concerns

// Business logic is completely separate
class CounterModel extends ChangeNotifier {
  // Pure Dart class - no Flutter widgets involved
}

This is similar to how you might separate data processing logic in your Python scripts from the presentation layer.

52.4.2 2. Two Ways to Access Data

context.watch() - Subscribe to changes:

final counter = context.watch<CounterModel>().counter;
// This widget rebuilds when CounterModel changes

context.read() - One-time access:

context.read<CounterModel>().increment();
// Just calls the method, doesn't listen for changes

52.4.3 3. Automatic Dependency Tracking

Provider automatically tracks which widgets depend on what data. When notifyListeners() is called, only widgets using context.watch rebuild.

52.5 Benefits Over InheritedWidget

Cleaner API:

  • No need to write updateShouldNotify
  • No need to create custom inherited widgets
  • Built-in change notification system

Better Performance:

  • Granular rebuilds - only widgets that watch specific data rebuild
  • Can watch specific properties instead of entire objects

Type Safety:

  • Generic types ensure you get the right model
  • Compile-time checking instead of runtime casting

Easier Testing:

  • Business logic (CounterModel) is pure Dart
  • Can unit test without any Flutter dependencies
  • Easy to mock in widget tests

52.6 Advanced Provider Usage

You can also use Selector for even more precise rebuilds:

class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Selector<CounterModel, int>(
      selector: (context, model) => model.counter,
      builder: (context, counter, child) {
        return Text('Count: $counter');
      },
    );
  }
}

This only rebuilds when the counter specifically changes, even if other properties in CounterModel change.

Provider essentially gives you Redux-like state management with much less boilerplate, making it perfect for Flutter apps of any size.

Would you like me to show you how to handle more complex state with multiple providers or async operations?