Complete guide to ChangeNotifier in Flutter

Image For ChangeNotifier in Flutter Post.

Flutter is one of the most popular cross-platform frameworks for mobile apps development in 2020. Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobileweb, and desktop from a single codebase. In this post, we will deep dive into ChangeNotifier in Flutter along with its corresponding topics like ChangeNotifierProvider, MultiProvider, Consumer, Provider etc.

Before discussing the need for ChangeNotifier we need to have a basic understanding of State in Flutter.

State in Flutter πŸ—½

State in basic terms is the description of the app’s current instance. The State remains constant however we in today’s world use dynamic apps. To achieve dynamic pages and apps we rebuild State again and again as needed. Flutter is very efficient and can handle multiple state updates at a given time. However, to use minimal resources, there’s a complete section of packages provided to achieve State Management in Flutter.

State is information that can be read synchronously when the widget is built and might change during the lifetime of the widget. It is the responsibility of the widget implementer to ensure that the State is promptly notified when such state changes, using State.setState.

flutter.dev

The traditional solution to State Management in Flutter is setState however, it comes with some drawbacks like deep coupling, expensive rebuilds, etc. There are other packages that provide simple and effective State Management like GetX, BLoC, RiverPod, Provider, etc. You can read more about State in my post on State Management in Flutter.

Let’s look at our traditional implementation first.

What we will build❓

Demo Project for ChangeNotifier in Flutter

Traditional Implementation 🎈

Step by Step Explanation:

  1. Firstly, we internally create a List<String> to store our data. Please note that the Stateful Widget contains the declaration of the list, now it is a private state to this Stateful Widget. Now if we want to use this list out of this page, we can not use it, as it is bound to the lifecycle of the widgets.
  2. Inside the onPressed of FloatingActionButton, we add another item to the to-do list and call setState to update the current State. Here, the thing to note is that we can not redirect the user to another page to add an item. This is because as soon as we move from this page the to-do list will be disposed of.
  3. Here, we simply display the contents of the list in a ListView.

This type of State is called Ephemeral State which is bound to a single page. While this code is able to add and show items, it is not much dynamic. It only has one page and can not persist/hold data across pages. While in most real-world applications, we persist the data along the screens to provide you with various functionalities, like modifying, deleting, adding, etc. This type of state that spans over two pages is called App State.

Lifting the State Up using Global Variables 🚞

As we just saw our list was bound to the lifecycle of the widget. To resolve this issue in a straightforward manner what we can do is declare our List as a global variable and access it on various other screens. This implementation also has some drawbacks which we will look into after the implementation:

Step by Step Explanation:

  1. Declare the list as a global variable so that its lifecycle is not bounded to the widget lifecycle and can be accessed from anywhere.
  2. Navigating to AddPage and whenever we return back to HomePage setState is invoked.
  3. Navigating to ModifyPage and when we return back setState is called to update the UI.
Image showing Lifting the State Up in Flutter. A subpart of ChangeNotifier in Flutter.
Lifting the State Up in Flutter

This implementation seems dynamic as it has 3 pages and on making changes on any of these pages the changes are reflected on the HomePage as well. However, there are some points where this implementation is wrong:

  1. Heavy Coupling between the HomePage, AddPage, and ModifyPage.
  2. If the user doesn’t add or modify the data, still we call setState on the HomePage.
  3. The UI is dynamic but it isn’t reactive.
  4. This implementation is like working against the Flutter framework. In Flutter it is preferred to inject dependencies needed into the widget tree.

ChangeNotifier in Flutter πŸ’«

ChangeNotifier is a class that provides notifications to its listeners whenever we want to notify them of changes. That means you can subscribe to a class that extends ChangeNotifier and calls its notifyListeners() method when there’s a change in the class. This call will notify all the listeners attached. ChangeNotifier is native to Flutter and is mostly used alongside Provider and RiverPod packages.

ValueNotifier is a ChangeNotifier that carries a single value and it will notify its listeners when its value property is changed. However, it only holds to a single value, when we use the class that extends ChangeNotifier we can define multiple values within it.

class MyValueNotifier extends ValueNotifier<int>{
  // Only holds single value
  int getValue() => value;
}

class MyChangeNotifier extends ChangeNotifier{
  // Can hold multiple values
}

Now let’s implement ChangeNotifier for our TODO app:

Ways to Consume ChangeNotifier in Flutter πŸ’Š

We have several ways to consume our change notifier in Flutter. Let’s discuss them and then we will implement them:

  1. Using .addListener method, as the ChangeNotifier is a type of Listenable.
  2. Another way is to consume the ChangeNotifier using the AnimatedBuilder, as it also takes a Listenable.
  3. Finally, we can use the combination of ChangeNotifierProvider, Consumer, and Provider. These all capabilities are provided to us by the Provider package.

The first and second method depend upon declaring the ChangeNotifier in global scope, while the third one injects the dependency class into the Widget Tree so that we can access it anywhere.

Before moving to implementation, just to ensure it doesn’t become cluttered. Remember that we have 3 pages, HomePage, AddPage, and ModifyPage. And every implementation will contain its own AddPage and Modify Page as well. Also, note that getAppBar and getListTile functions are declared in a class by the name widgets.dart. These functions are declared separately as they are the same across all the implementations.

# lib/widgets.dart

import 'package:flutter/material.dart';

AppBar getAppBar(String title) {
  return AppBar(
    centerTitle: true,
    elevation: 0,
    title: Text(title),
  );
}

ListTile getListTile(List<String> items, int index) {
  return ListTile(
    leading: CircleAvatar(child: Text(index.toString())),
    contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0),
    title: Text(items[index]),
  );
}

Consuming Using .addListener((){}) πŸ‘‚

As you can see this approach also declare the ItemNotifier as a global variable. However, note that we only listen to valid events by adding a listener to the ItemNotifier. When we manually add a listener it is a must to dispose of it when not in need. However, in this approach, we are not injecting our dependency into the widget tree, it is different from the widget tree in the global scope.

Consuming using AnimatedBuilder πŸ‘·β€β™‚οΈ

As we discussed, AnimatedBuilder takes in any Listenable as a parameter. The ChangeNotifier class also extends Listenable thus we can simply use the AnimatedBuilder.

Here also we declare the ItemNotifier global, and it is similar to listening but we don’t have to call setState manually here. The AnimatedBuilder internally manages the subscription and thus we don’t need to dispose of the ItemNotifier listener here.

The two approaches we discussed do not inject the dependency into the widget tree, the dependency exists in the global scope. Now we will look at ChangeNotifierProvider and Provider in Flutter. They provide powerful dependency injection, such that the dependency is available throughout the widget tree.

Image showing dependency injection, A subpart of ChangeNotifier in Flutter.
Dependency Injection in Flutter

ChangeNotifierProvider in Flutter πŸŒ€

The ChangeNotifierProvider is built to provide finer use cases to ChangeNotifier in Flutter. Like other builders like AnimatedBuilder, StreamBuilder, FutureBuilder, ValueListenableBuilder, etc. It is like a builder which accepts ChangeNotifier and updates its children of any value changes.

To use ChangeNotifierProvider in Flutter, we need to add the Provider dependency to our pubspec.yaml.

Add the latest Provider dependency:

 provider: ^6.0.2

In our case the, ChangeNotifierProvider<T> is wrapped around MaterialApp. Please don’t pollute the scope with dependencies that are not needed in the sub-lying Widget Tree. In our case, we only have 3 pages, and all need access to the same dependency so for the sake of simplicity, we wrap it around our MaterialApp.

Dependency Injection πŸ§ͺ

ChangeNotifierProvider(
      create: (context) => ItemNotifier(),
      child: MaterialApp(...),
);

As you can clearly see it also follows the Singleton pattern the instance is created once in the create method and is made available to the underlying widgets. ChangeNotifierProvider is a simplification of the InheritedWidgets in Flutter. Here, we have injected the ItemNotifier into the Widget Tree now we can access it in our application.

We can also inject multiple dependencies into the Widget Tree using the MultiProvider:

MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => ItemNotifier()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: const MyApp(),
);

Note that ChangeNotifierProvider accepts a ChangeNotifier class while, the Provider accepts a simple class. Provider is used in cases where we want to share the values, but don’t want to update the listeners.

Learn more

Accessing our Dependency 🦾

There are two ways to access our dependency one is using the Consumer<T> widget and another is using the Provider.of<T>(context).

1. Using Consumer<T>

We use the Consumer<T> when we want to rebuild the widgets when a value changes. It is a must to provide the type <T> so that the Provider can understand which dependency you are referring to.

Consumer<ItemNotifier>(
  builder: (context, value, child) {...},
  child: SomeWidget(),
);

The Consumer widget takes two parameters, the builder parameter is mandatory and the child parameter is optional. The child parameter is any expensive widget that does not get affected by any Change in the ChangeNotifier.

2. Using Provider.of<T>(context)

The Provider.of<T>(context) is used when you need to access the dependency but you don’t want to make any changes to the User Interface. We can use the Consumer<T> but that would be waste of resources. We simply set the listen to false signifying that we don’t need to listen to updates from the ChangeNotifier.

Provider.of<ItemNotifier>(context, listen: false).delete(0);

Final Implementation πŸ“

As you can see, we use Consumer where we need to update the UI. The Provider.of(context) is used where we don’t need any further notifications of the Changes made thus we set the listen parameter to false and use the functions provided in the ItemNotifier class.

Conclusion πŸ‘©β€πŸŒΎ

In this post, we looked at the basics of ChangeNotifier and ChangeNotifierProvider. We discussed how to create and consume ChangeNotifier in Flutter. ChangeNotifer is a native feature to Flutter, i.e you don’t need to add any dependencies to use it, however it is often used along with Provider to provide advanced functionality.

Now that you know about ChangeNotifier in Flutter, you should check these posts next:

If you have any queries you can comment below and I will be happy to help you. If you are new to Flutter check my post on how to install Flutter on windows?

1 comment
Leave a Reply

Your email address will not be published.

Previous Post
Custom ValueNotifier in Flutter

Custom ValueNotifier in Flutter – With Examples

Next Post
State Management in Flutter

What is State Management in Flutter

Related Posts