flutter
/

Flutter Provider – A Complete Guide to State Management

Last Sync: Today

On this page

14
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Flutter Provider – A Complete Guide to State Management

What is Provider?

Provider is a wrapper around InheritedWidget that makes it easier to manage and share state across your Flutter app. It is the recommended state management solution for many applications because it's simple, scalable, and integrates well with the framework. Provider allows you to expose a model (e.g., a ChangeNotifier) to the widget tree, and listen to changes in a very efficient way – only widgets that depend on the data are rebuilt when it changes.

Why Use Provider?

    • Simple: Minimal boilerplate compared to other state management solutions.
    • Scalable: Works well for both small apps and large projects.
    • Efficient: Only rebuilds widgets that consume the data.
    • Testable: Easy to mock providers for unit tests.
    • Official recommendation: Provider is the go‑to solution for many Flutter developers and is backed by the Flutter team.

Adding Provider to Your Project

Add the provider package to your pubspec.yaml:

dependencies:
  provider: ^6.1.0

Then run flutter pub get.

Creating a Model with ChangeNotifier

Provider works best with classes that extend ChangeNotifier. The notifier holds the state and notifies listeners when it changes using notifyListeners().

DARTRead-only
1
import 'package:flutter/material.dart';

class Counter extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // Tell listeners to rebuild
  }

  void decrement() {
    _count--;
    notifyListeners();
  }
}

Providing the Model

Wrap your app (or part of it) with a ChangeNotifierProvider. This makes the model available to all descendant widgets.

DARTRead-only
1
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MyApp(),
    ),
  );
}

You can also use Provider.value if you already have an instance, but create is the recommended way because it ensures the provider is disposed when no longer needed.

Consuming the Model

There are several ways to access the provided value: Consumer, Provider.of, and context.watch / context.read (with provider >= 5.0.0).

  1. Consumer Widget

Consumer rebuilds only the part of the widget tree that depends on the data. It's the most efficient way to listen to changes.

DARTRead-only
1
Consumer<Counter>(
  builder: (context, counter, child) {
    return Text('Count: ${counter.count}');
  },
)

  1. Provider.of

Provider.of<T>(context) gives you access to the model. With listen: false, it doesn't rebuild when the model changes; with listen: true (default), it does rebuild. Use listen: false for actions like increment.

DARTRead-only
1
// Reading (rebuilds when data changes)
final counter = Provider.of<Counter>(context);

// Reading without listening (e.g., for actions)
final counter = Provider.of<Counter>(context, listen: false);
counter.increment();

  1. context.watch / context.read (Provider 5.0+)

These extension methods are even more concise: context.watch<T>() listens to changes, context.read<T>() does not.

DARTRead-only
1
// To rebuild when data changes
final counter = context.watch<Counter>();

// To call methods without rebuilding
context.read<Counter>().increment();

MultiProvider for Multiple Models

When you have several models, use MultiProvider to combine them.

DARTRead-only
1
MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (context) => Counter()),
    ChangeNotifierProvider(create: (context) => Settings()),
  ],
  child: MyApp(),
)

ProxyProvider for Dependent Models

If one model depends on another, use ProxyProvider. It rebuilds the dependent provider when the source changes.

DARTRead-only
1
class UserRepository {
  final ApiService api;
  UserRepository(this.api);
}

MultiProvider(
  providers: [
    Provider(create: (context) => ApiService()),
    ProxyProvider<ApiService, UserRepository>(
      update: (context, api, previous) => UserRepository(api),
    ),
  ],
  child: MyApp(),
)

Best Practices

    • Use ChangeNotifier for models: It's simple and efficient.
    • Place providers high in the tree: Usually just below MaterialApp to make them available everywhere.
    • Use Consumer or context.watch only where needed to limit rebuilds.
    • For actions (like increment), use context.read to avoid unnecessary rebuilds.
    • Use MultiProvider for multiple providers to keep the tree clean.
    • Dispose providers when needed: ChangeNotifierProvider automatically disposes the notifier; for custom providers, use dispose.

Common Mistakes

    • Calling notifyListeners unnecessarily – only call it when state actually changes.
    • Using Provider.of with listen: true in places where you don't need to rebuild – this can cause performance issues.
    • Providing the same model multiple times – ensures the same instance is used throughout the app.
    • Not disposing resources – if your model has streams or controllers, dispose them in the model's own dispose method.
    • Over‑complicating with ProxyProvider when not needed – use simple providers first, then add complexity as needed.

Key Takeaways

    • Provider is a simple, scalable state management solution.
    • Use ChangeNotifier and ChangeNotifierProvider to expose state.
    • Consume state with Consumer, context.watch, or Provider.of.
    • Use MultiProvider for multiple models, ProxyProvider for dependencies.
    • Always call notifyListeners() after changing state to update the UI.

Try it yourself

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

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MyApp(),
    ),
  );
}

class Counter extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }

  void decrement() {
    _count--;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Provider Demo')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Consumer<Counter>(
                builder: (context, counter, child) {
                  return Text('Count: ${counter.count}', style: TextStyle(fontSize: 24));
                },
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: () => context.read<Counter>().decrement(),
                    child: Text('-'),
                  ),
                  SizedBox(width: 20),
                  ElevatedButton(
                    onPressed: () => context.read<Counter>().increment(),
                    child: Text('+'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 4

Which class should you extend to create a model for Provider?

A
ChangeNotifier
B
StatefulWidget
C
InheritedWidget
D
Bloc
Q2
of 4

How do you expose a model to the widget tree?

A
Wrap with `Provider`
B
Wrap with `ChangeNotifierProvider`
C
Pass it as a constructor argument
D
Use `InheritedWidget` directly
Q3
of 4

Which method is used to trigger a rebuild of listeners?

A
update()
B
rebuild()
C
notifyListeners()
D
setState()
Q4
of 4

What is the purpose of `context.read`?

A
To listen to changes and rebuild
B
To call methods without causing a rebuild
C
To read a provider and cache the value
D
To dispose the provider

Frequently Asked Questions

What is the difference between `Provider` and `ChangeNotifierProvider`?

Provider is the base class; it can provide any value. ChangeNotifierProvider is a specialized subclass that automatically calls dispose on the notifier when the provider is removed. It's the preferred way to provide a ChangeNotifier.

When should I use `context.watch` vs `context.read`?

Use context.watch when you need to rebuild the widget when the model changes (e.g., to display a counter value). Use context.read when you only need to call a method on the model and do not need to rebuild (e.g., increment button).

Can I use Provider with other state management solutions?

Yes, Provider can be used alongside other solutions like BLoC or Riverpod, but it's usually best to pick one for consistency. Provider is often used with ChangeNotifier for simple state, while BLoC is better for complex business logic.

How do I test a widget that uses Provider?

Wrap the widget with the appropriate provider in your test. For example, ChangeNotifierProvider.value(value: myMock, child: MyWidget()). Use a mock of your notifier to control its behavior.

What happens if I call `notifyListeners` too often?

It causes unnecessary rebuilds, which can affect performance. Only call notifyListeners when the state actually changes, and consider using shouldRebuild or Selector for fine‑grained control if needed.

Previous

flutter setstate

Next

flutter riverpod

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.