Skip to main content

Using Provider for State Management

1. Understanding ChangeNotifier Class

The ChangeNotifier is a core class in Flutter's state management system that enables widgets to listen for changes in data and rebuild accordingly. It is part of the flutter/src/foundation/change_notifier.dart library.

Property/Method Description Notes
addListener Adds a listener to the notifier. Listeners are called whenever notifyListeners is invoked.
removeListener Removes a listener from the notifier. Prevents memory leaks if listeners are not properly removed.
notifyListeners Notifies all registered listeners of a change. Must be called manually to trigger rebuilds in widgets using ChangeNotifierProvider.

Example: Basic Usage of ChangeNotifier

import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/change_notifier.dart';

class Counter with ChangeNotifier {
  int _value = 0;

  int get value => _value;

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

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => Counter(),
      child: Consumer<Counter>(
        builder: (context, counter, child) {
          return Text('Count: ${counter.value}');
        },
      ),
    );
  }
}
Note: ChangeNotifier is commonly used in conjunction with ChangeNotifierProvider from the provider package to manage state in Flutter applications.

2. Creating ChangeNotifier Models

Property Description Usage
ChangeNotifier Base class for models that notify listeners when state changes Extend ChangeNotifier to create state models
notifyListeners() Method to call when state changes to trigger UI updates Call after modifying model state

Example: Basic ChangeNotifier Model

import 'package:flutter/material.dart';

class CounterModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

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

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

Example: Model with Multiple State Properties

class UserProvider extends ChangeNotifier {
  String _name = '';
  bool _isLoggedIn = false;

  String get name => _name;
  bool get isLoggedIn => _isLoggedIn;

  void login(String name) {
    _name = name;
    _isLoggedIn = true;
    notifyListeners();
  }

  void logout() {
    _name = '';
    _isLoggedIn = false;
    notifyListeners();
  }
}
Note: Always call notifyListeners() after modifying state to ensure UI updates.

3. Using notifyListeners() Method

The notifyListeners() method in Flutter's Provider is used to notify all listeners of a change in the state of a ChangeNotifier object. It is essential for updating UIs when the state changes.

Method Description Usage
notifyListeners() Triggers a rebuild of all widgets that are listening to this provider. Call this method whenever the state changes to inform listeners.

Example: Using notifyListeners()

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

class Counter with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // Notifies all listeners of the change
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => Counter(),
      child: Scaffold(
        appBar: AppBar(title: Text('Counter Example')),
        body: Center(
          child: Consumer<Counter>(
            builder: (context, counter, child) =>
                Text('Count: ${counter.count}'),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            Provider.of<Counter>(context, listen: false).increment();
          },
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

4. Using Provider Package

The provider package is a popular state management solution in Flutter that uses the ChangeNotifier and ChangeNotifierProvider to manage and propagate state changes efficiently.

Feature Description Usage
ChangeNotifier Base class for state objects that notify listeners when changes occur. class Counter with ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); } }
ChangeNotifierProvider Provides a ChangeNotifier to its descendants in the widget tree. ChangeNotifierProvider(create: (context) => Counter(), child: MyApp())
Consumer Rebuilds widgets when the state it listens to changes. Consumer<Counter>(builder: (context, counter, child) => Text(counter.count.toString()))

Example: Basic Counter with Provider

// Counter model
class Counter with ChangeNotifier {
  int _count = 0;

  int get count => _count;

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

// Main app
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('Provider Example')),
          body: Center(
            child: Consumer<Counter>(
              builder: (context, counter, child) =>
                  Text('Count: ${counter.count}'),
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => context.read<Counter>().increment(),
            child: Icon(Icons.add),
          ),
        ),
      ),
    );
  }
}
Note: The Consumer widget rebuilds only the part of the UI that depends on the state, improving performance.

Provider Package Benefits

  • Easy to integrate with existing Flutter apps.
  • Supports multiple providers in the same app.
  • Provides built-in debugging tools for state changes.

5. Accessing Providers

Provider Access Method Description
ChangeNotifierProvider Provider.of<MyModel>(context) Access a ChangeNotifier model from the widget tree
ValueListenableProvider Provider.of<MyModel>(context) Access a ValueListenable model from the widget tree
StreamProvider Provider.of<MyModel>(context) Access a Stream model from the widget tree
FutureProvider Provider.of<MyModel>(context) Access a Future model from the widget tree

Example: Accessing a ChangeNotifier Provider

class MyModel extends ChangeNotifier {
  int _counter = 0;
  int get counter => _counter;

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

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyModel(),
      child: Consumer<MyModel>(
        builder: (context, model, child) {
          return Text('Counter: ${model.counter}');
        },
      ),
    );
  }
}

Example: Accessing a Stream Provider

Stream<String> fetchUserData() async* {
  yield 'User Data';
}

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamProvider<String>(
      create: (context) => fetchUserData(),
      child: Consumer<String>(
        builder: (context, userData, child) {
          return Text('User: $userData');
        },
      ),
    );
  }
}
Note: Use context.watch<MyModel>() for automatic rebuilds when the provider changes.
Warning: Avoid calling Provider.of without a context that is a descendant of the provider.

6. Using Consumer Widget

The Consumer widget in Flutter is a powerful way to listen to changes in a ChangeNotifier and rebuild only the parts of the UI that depend on that data. It's particularly useful when you want to access a provider's state without wrapping your entire widget tree in a Provider or ListenableBuilder.

Property Description Example
provider The provider to listen to. This can be a ChangeNotifierProvider, ValueListenableProvider, or any other provider that implements Listenable. provider: context.watch<MyProvider>()
builder A function that builds the widget tree. It receives the context and the provider instance as arguments. (context, myProvider) => Text(myProvider.data)

Example: Using Consumer to Display Data

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<MyProvider>(
      builder: (context, provider, child) {
        return Text(provider.data);
      },
    );
  }
}

Example: Using Consumer with Multiple Providers

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer2<ProviderA, ProviderB>(
      builder: (context, providerA, providerB, child) {
        return Text('${providerA.data} - ${providerB.data}');
      },
    );
  }
}
Note: The Consumer widget is efficient because it only rebuilds the part of the UI that depends on the provider's state, not the entire widget tree.
Warning: Avoid using Consumer inside a Provider or ListenableBuilder as it can lead to unnecessary rebuilds.

Key Points About Consumer

  • Efficiency: Only rebuilds the part of the UI that depends on the provider's state.
  • Flexibility: Can be used with multiple providers using Consumer2, Consumer3, etc.
  • Context: The builder function receives the context and the provider instance.

7. Using Selector Widget

The Selector widget in Flutter allows you to efficiently rebuild only the parts of your UI that depend on specific values from a ChangeNotifier or Provider. It's particularly useful for optimizing performance in apps with frequent state updates.

Property Description Example
selector A function that extracts a specific value from the provider to watch for changes. (context) => context.watch<MyProvider>().value
builder A function that builds the widget tree using the selected value. (context, value) => Text(value)
shouldRebuild An optional function to control whether the widget should rebuild based on a new value. (oldValue, newValue) => oldValue != newValue

Example: Using Selector to Watch Specific Value

class MyProvider extends ChangeNotifier {
  int _counter = 0;
  int get counter => _counter;

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

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Selector<MyProvider, int>(
      selector: (context, provider) => provider.counter,
      builder: (context, counter, child) {
        return Text('Counter: $counter');
      },
    );
  }
}
Note: The Selector widget only rebuilds when the selected value changes, which improves performance by avoiding unnecessary rebuilds of the entire widget tree.

8. Using MultiProvider Widget

The MultiProvider widget is a convenient way to wrap multiple providers in a single widget, simplifying the setup of state management in Flutter applications.

Property Description Example
providers List of providers to wrap [ChangeNotifierProvider<Counter>(create: (context) => Counter()), Provider<Theme>(create: (context) => Theme())]
child Child widget that will have access to the providers MaterialApp(home: MyHomePage())

Example: Using MultiProvider

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

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider<Counter>(create: (context) => Counter()),
        Provider<Theme>(create: (context) => Theme()),
      ],
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

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

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

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = context.watch<Counter>();
    return Scaffold(
      appBar: AppBar(title: Text('MultiProvider Example')),
      body: Center(
        child: Text('Count: ${counter.count}'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: counter.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}
Note: MultiProvider is particularly useful when managing multiple independent providers, reducing boilerplate and improving readability.

Key Points

  • MultiProvider wraps multiple providers in a single widget.
  • It simplifies the setup of state management by reducing nesting.
  • Each provider in the list is accessible to child widgets via context.watch or context.read.

9. Using ProxyProvider Widget

The ProxyProvider widget in Flutter is used to create a provider that depends on another provider. It's particularly useful when you need to transform or compute data from one provider to make it available to other parts of your app.

Property Description Example
child The child widget that will have access to the provided data. child: MyWidget()
update A function that returns the value to be provided, based on the source provider. update: (context, value) => value * 2
builder Function that builds the provider with the computed value. builder: (context, value) => MyProvider(value)

Example: Using ProxyProvider to Transform Data

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

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

class DoubleCounterProvider extends ChangeNotifier {
  int _doubleCount = 0;
  int get doubleCount => _doubleCount;

  void update(int count) {
    _doubleCount = count * 2;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => CounterProvider(),
      child: ProxyProvider<CounterProvider, DoubleCounterProvider>(
        update: (context, counter, doubleCounter) {
          doubleCounter.update(counter.count);
        },
        child: MaterialApp(
          home: Scaffold(
            body: Center(
              child: Consumer<DoubleCounterProvider>(
                builder: (context, doubleCounter, child) {
                  return Text('Double Count: ${doubleCounter.doubleCount}');
                },
              ),
            ),
          ),
        ),
      ),
    );
  }
}
Note: ProxyProvider is useful for creating derived state from existing providers. It allows you to compute new values based on the data from another provider and make that computed value available to other parts of your app.

10. Implementing Repository Pattern with Provider

Example: Basic Repository Implementation

// Repository interface
abstract class UserRepository {
  Future<User> fetchUser(String id);
  Future<List<User>> fetchUsers();
}

// Concrete implementation
class ApiUserRepository implements UserRepository {
  final HttpClient client;

  ApiUserRepository(this.client);

  @override
  Future<User> fetchUser(String id) async {
    final response = await client.get('/users/$id');
    return User.fromJson(response.body);
  }

  @override
  Future<List<User>> fetchUsers() async {
    final response = await client.get('/users');
    return (response.body as List)
        .map((e) => User.fromJson(e))
        .toList();
  }
}
Component Description Usage
Repository Encapsulates data access logic Used by providers to fetch data
Provider Manages state and exposes data to widgets Wraps repository in a stateful context

Example: Provider Integration

// In main.dart or app setup
final userRepo = ApiUserRepository(HttpClient());

// Wrap with Provider
Provider.value(
  value: userRepo,
  child: MyApp(),
);

// In widget using the repo
final repo = Provider.of<UserRepository>(context);
final user = await repo.fetchUser('123');
Note: The repository pattern decouples data sources from UI logic, making tests easier and allowing for multiple data sources (e.g., local cache + API).

11. Managing App-Wide State

Provider is a popular state management solution in Flutter that allows you to manage and share state across your app efficiently. It provides a way to access data from any part of the widget tree without passing data down through multiple layers.

Provider Type Description Usage Example
ChangeNotifierProvider Used for managing state that changes and notifies listeners when updated. ChangeNotifierProvider<CounterModel>(create: (context) => CounterModel(),child: MyApp(),)
ValueListenableProvider Used for providers that listen to a ValueListenable object. ValueListenableProvider<int>(create: (context) => ValueNotifier(0),child: MyApp(),)
StreamProvider Used for providers that listen to a Stream. StreamProvider<User>(create: (context) => authStream,child: MyApp(),)
FutureProvider Used for providers that provide data from a Future. FutureProvider<UserData>(create: (context) => fetchUserData(),child: MyApp(),)

Example: Using ChangeNotifierProvider to manage app-wide state

class CounterModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

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

// In your app widget tree
ChangeNotifierProvider<CounterModel>(
  create: (context) => CounterModel(),
  child: MyApp(),
)

Example: Accessing state in a widget

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<CounterModel>(
      builder: (context, counter, child) {
        return Text('Count: ${counter.count}');
      },
    );
  }
}
Note: When using ChangeNotifierProvider, it's important to call notifyListeners() whenever the state changes to ensure that widgets listening to this provider are updated.
Warning: Avoid using ChangeNotifierProvider for large state objects or frequent updates as it can lead to performance issues. Consider using Selector to optimize rebuilds.

Key Points About App-Wide State Management with Provider

  • Centralized State: All app-wide state is managed in one place, making it easier to maintain and debug.
  • Efficient Updates: Widgets only rebuild when the specific state they depend on changes.
  • Flexibility: Multiple provider types allow for various state management needs (e.g., streams, futures).
  • Easy Integration: Provider integrates seamlessly with Flutter's widget tree and supports reactive updates.

Was this article helpful?