David Serrano Canales
David Serrano

David Serrano

Flutter State Management made easy with provider | Flutter Tutorial for Beginners

Flutter State Management made easy with provider | Flutter Tutorial for Beginners

Basic and simple introduction to understand state management in Flutter with provider.

David Serrano Canales
·Apr 27, 2022·

6 min read

In this article I am going to teach you in an extremely simple and short way how to apply state management in a Flutter app with the provider package.

📽 Video version available on YouTube and Odysee

The resulting application is going to be a counter app in which we can increase the counter if we press the button located at the bottom of the screen:

counter_app_blog.png

Does this example sound familiar to you? Yes, what we are going to do is refactor the app template that Flutter creates when you create a project, leaving the functionality and the visual part intact, but refactoring its state management with provider.

Provider architecture

Before we start I'm going to show you the change we're going to make. The default state management that Flutter gives you when you create a project is as follows:

classic_widget_blog.png

In this diagram we see a widget that is responsible for the visual part and state management. This management is done through variables in the body of the class, when we want to change the state we use the setState() method.

The change that we are going to apply consists of refactoring this state management so that it looks like this:

reactive_pattern_blog.png

On the left side we have a model class that will contain the variables that will constitute the state of our widget, this widget will subscribe to this model to receive state changes, and when those changes are made the model will emit the updated values to the widget. By doing so we are creating a circular reactive architecture.

A great advantage of this model is that we have a clear separation between the state logic and the display of views, thus achieving a much cleaner code that is easier to organize and easier to maintain.

In addition, with this methodology we could also achieve the following:

multiple_listeners_blog.png

In this diagram you can see how not just one, but multiple widgets can subscribe to the model class for all of them to receive state changes. A practical example of this would be for example a class that manages user authentication against a server, you could have a single class that contains methods like login(), register(), getUser()... and then several widgets that consume these changes and that are automatically modified when a state change occurs due to user authentication.

Create the model that manages the state

First, create an application to work on:

flutter create state_management_provider

Open the created app in your trusted IDE and modify pubspec.yaml to include the provider dependency:

name: state_management_provider
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: ">=2.16.2 <3.0.0"

dependencies:
  flutter:
    sdk:flutter
  cupertino_icons: ^1.0.2
  provider: ^6.0.2

dev_dependencies:
  flutter_test:
    sdk:flutter
  flutter_lints: ^1.0.0

flutter:
  uses-material-design: true

Download the dependencies by running flutter pub get or through your IDE.

Now if we go to lib/main.dart we will see that the state is being managed with a _counter variable. We are going to create a new class in a file called main_model.dart that extends ChangeNotifier, by extending this class it will allow other classes to subscribe to it and receive state changes when calling the notifyListeners() method. Let's move the _counter variable into this class and add a getter and a setter to it as follows:

// lib/main_model.dart

import 'package:flutter/material.dart';

class MainModel extends ChangeNotifier {
  int _counter = 0;

  int get counter => _counter;

  set counter(int value) {
    if (value != _counter) {
      _counter = value;
      notifyListeners();
    }
  }
}

Here you can see that we still have a private variable _counter initialized with the value 0. Through its getter (marked with the keyword get) the widgets can obtain this value at any time. Through its setter (marked with the keyword set) the widgets can update their value, and when this happens first we will check that the new value is different from the previous one, if it is, we update the private variable that stores the real value and invoke the notifyListeners() method so that all widgets that have subscribed to this class are notified so their view can be re-rendered with the new value.

Refactor the view so it consumes the model

We are now going to apply changes to main.dart so that we can use the model class we just created. First of all, remove all comments and also the current state management in this class:

// lib/main.dart

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

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

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

At this point the IDE will highlight two errors, this is because we have removed the _counter variable and the _incrementCounter method.

In order to be able to use the model class that we have created, we first need a widget that provides it to the entire widget tree that hangs from it, and that widget is called ChangeNotifierProvider, included in the provider package. This component allows us to create a class that extends ChangeNotifier so that any widget under it can get it.

Also, in order to react to changes, we're going to need a widget that can read those changes and react. This widget is called Consumer.

The combination of both components look like this:

    ChangeNotifierProvider<MainModel>(
      create: (_) => MainModel(),
      child: Consumer<MainModel>(
        builder: (context, model, child) {

          // Here you can use the model variable 
          // to read and alter the state

        },
      ),
    )

We are going to modify the build() method with all of the above and we are going to use the model variable that the Consumer gives us to read the current value of the counter and also to modify it:

@override
Widget build(BuildContext context) {
  return ChangeNotifierProvider(
    create: (_) => MainModel(),
    child: Consumer<MainModel>(
      builder: (context, model, child) => Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '${model.counter}',
                style: Theme.of(context).textTheme.headline4,
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => model.counter++,
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
      ),
    ),
  );
}

Note that in this case we don't need the method we used before to modify the state: setState(). Now we only need to set the new value and the model class will take care of propagating this change, the Consumer will receive this change and will re-draw our UI.

Conclusion

In this small example we have implemented state management with provider in the simplest and most basic way possible. The provider package offers many more possibilities and resources to do more advanced things, but we can leave that for another day.

If you have found this article useful and you would like me to do more tutorials explaining more in-depth state management with provider and other possible applications, please let me know by writing below in the comment box.

You can find the final app of this tutorial here.

Thanks for reading this far and happy coding :)

Did you find this article valuable?

Support David Serrano Canales by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors