State Management, be it in Android or iOS, is the most important part in any app development project. By managing state you make sure that data reaches where it is required.

State management is a strategic approach to organising all user interactions such that the created system state may be changed if any changes are needed to the user interface, databases, or other parts of the system.

Flutter has a built-in state management mechanism called setState(), however utilising it has the big downside of re-rendering the whole user interface (UI). The main flaw with this system is that even when a portion of the user interface doesn’t need to be re-rendered, it still is, which reduces efficiency and may result in high render latency or janky behaviour. Also we aren’t able to separate views and business logic with this approach so a better approach is to use Provider for state management.

State management with Provider

The components of Provider which come together to manage state are:

ChangeNotifier: This is the class you will need to extend in order to use the functionalities of Provider. This stores all the state and business logic of the app, and whenever the state is updated the consuming widgets can be notified.

Consumer: This widget can be used to listen to state changes and update UI accordingly, while using this widget we need to explicitly mention which kind of state changes we are expecting. To listen to changes this widget has to be parent widget of the widget tree where we are expecting changes. Also we can specify what part of widget tree is static widget tree so that only required part of widget tree is updated.

ChangeNotifierProvider: This widget makes the state holding classes(classes which extended ChangeNotifier) accessible to its children widgets. Thus this class connects ChangeNotifier with Consumer. Consumer has to have a ChangeNotifierProvider widget (of the type the consumer expects)in the parent widget tree otherwise an error will be thrown.

Implementation

Enough of “Theory”, Let’s implement and see what all this fuss is about!

Let us use the default counter example that flutter provides when we create any project in Flutter and modify it a bit to provide decrement as an additional feature. We modify the stateful widget to a stateless widget in order to demonstrate the power of provider. We will be following MVVM architecture.

FlutterUI

Counter Screen without provider:
import 'package:flutter/material.dart';

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Counter"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              'Count: 0',
              style: Theme.of(context).textTheme.headline4,
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                GestureDetector(
                  onTap: () {},
                  child: Container(
                    height: 50,
                    width: 50,
                    decoration: const BoxDecoration(color: Colors.green, shape: BoxShape.circle),
                    child: const Icon(
                      Icons.add,
                      color: Colors.white,
                    ),
                  ),
                ),
                GestureDetector(
                  onTap: () {},
                  child: Container(
                    height: 50,
                    width: 50,
                    decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
                    child: const Icon(
                      Icons.remove,
                      color: Colors.white,
                    ),
                  ),
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

We have a row with two clickable containers, one to increment the counter and another to decrement the counter. Currently this code isn’t responsive as it does nothing when pressed, but we will be implementing the increment logic using provider.

State Handler (ChangeNotifier)

Let us now also create a ChangeNotifier that would have all state related data and business logic.

Change Notifier code for Counter Screen:
import 'package:flutter/material.dart';

class CounterProvider with ChangeNotifier {
  int _counter = 0;

  get counter {
    return _counter;
  }

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

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

Now in the above ChangeNotifier we have a single integer variable “counter” whose state has to be maintained. We have a getter which will return the value of counter, we also have support for increment and decrement. Note how we call notifyListerners() everytime there is a change in the value of counter. This lets the Consumers know that the state has changed and to rebuild and show new and updated data to the user.

Thus, notifyListerners() is the most important part of the code because if we forget to write this one line then all of the state changes that we do wouldn’t reflect in the UI.

Connecting the UI and state handler

Now, we have the CounterProvider which extends ChangeNotifier to provide with state changes to CounterScreen to show the changes. We need to connect these two using ChangeNotifierProvider, also a widget, that provides an instance of ChangeNotifier to the screen of a particular type, here CounterProvider.

...
return ChangeNotifierProvider<CounterProvider>(
create: (context) => CounterProvider(),
child: Scaffold(
...

In the build method, instead of directly returning the Scaffold we make the above changes and so our State provider is set to function properly. Now we only need to use Consumer widget to utilise the state provided.

Consumer

In the above code we are explicitly mentioning that the type of instance provided is CounterProvider(), so we can only ask for CounterProvider() while using Consumer. If we try to ask for a different type, error will be thrown.

Counter Screen with provider:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_example/CounterProvider.dart';

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CounterProvider>(
      create: (context) => CounterProvider(),
      child: Scaffold(
        appBar: AppBar(
          title: const Text("Counter"),
        ),
        body: Consumer<CounterProvider>(
          builder: (context, provider, child) => Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  'Count: ${provider.counter}',
                  style: Theme.of(context).textTheme.headline4,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    GestureDetector(
                      onTap: () => provider.increment(),
                      child: Container(
                        height: 50,
                        width: 50,
                        decoration: const BoxDecoration(color: Colors.green, shape: BoxShape.circle),
                        child: const Icon(
                          Icons.add,
                          color: Colors.white,
                        ),
                      ),
                    ),
                    GestureDetector(
                      onTap: () => provider.decrement(),
                      child: Container(
                        height: 50,
                        width: 50,
                        decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
                        child: const Icon(
                          Icons.remove,
                          color: Colors.white,
                        ),
                      ),
                    )
                  ],
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

On line 16, we add a Consumer widget which provides access to CounterProvider instance to the builder widget tree. Anything inside the consumer widget gets rebuilt whenever notifyListeners() is called from inside that particular type of provider. The child parameter is any widget that doesn’t need the data inside of the provider, so when the data gets updated, they don’t get re-created since they don’t need the data, rather they are passed as a reference to the builder.

Here is the code in main.dart file

main.dart code
import 'package:flutter/material.dart';

import 'counterScreen.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

That’s all there is to it. Thank you for reading.