A Deep Dive into ValueNotifier

ValueNotifier updates the UI whenever there are changes in it's value. It's our go to solution for State Management in Flutter.

ValueNotifier

Today, we are going to dive deep into ValueNotifier, including its functionality and how it works under the hood.

We start out in the element tree.

If you take a look at the implementation of the StatelessWidget, you will find references to elements.

abstract class StatelessWidget extends Widget {
/// Initializes [key] for subclasses.
const StatelessWidget({super.key});
/// Creates a [StatelessElement] to manage this widget's location in the tree.
///
/// It is uncommon for subclasses to override this method.
@override
StatelessElement createElement() => StatelessElement(this);
// rest of class
}

A Flutter app is a tree of widgets. This widget tree then gets turned into an element tree and eventually into a render tree.

The element is a critical part of the rendering pipeline. It provides the bridge between the widget and the render object.

We will cover elements and the element tree in depth in a separate article, but for now, you just need to know that it performs the underlying logic in your Flutter apps.

When writing a Flutter application, to make widgets re-render, you would either use setState or a state management package.

Both of these options are doing the same thing under the hood. They call a markNeedsBuild function within the Element.

The core purpose of the markNeedsBuild method is to mark the element as dirty and schedule a new frame, which will result in our build method being re-run.

class CustomElement extends Element {
@override
void update(covariant Widget newWidget) {
super.update(newWidget);
markNeedsBuild(); // Notify Flutter that this element needs to rebuild
}
}

As a little side note, if you look at the implementation of an element, you will see that it implements the BuildContext, which you have access to directly through your widget.

abstract class Element extends DiagnosticableTree implements BuildContext {
/// Creates an element that uses the given widget as its configuration.
///
/// Typically called by an override of [Widget.createElement].
Element(Widget widget)
: _widget = widget {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated(
library: _flutterWidgetsLibrary,
className: '$Element',
object: this,
);
}
}
// rest of file
}

Now, you would never do this in a real project, but you can call this rebuild method directly in your Flutter code. You can take the BuildContext, cast it as an Element, and call markNeedsBuild.

Now you have built setState from scratch… kind of.

onTap: () {
(context as Element).markNeedsBuild();
},

setState

This all leads us to setState. setState allows us to call the markNeedsBuild method from a StatefulWidget.

This is the implementation of the setState method. If you look at the bottom of the function, voila, it’s calling markNeedsBuild on the element.

@protected
void setState(VoidCallback fn) {
// rest of method
final Object? result = fn() as dynamic;
// rest of method
_element!.markNeedsBuild();
}

For now, we have explored setState and what is happening under the hood to cause a re-render in a Flutter application.

But ValueNotifier doesn’t use setState. So how does that work?

Course thumbnail

Want to learn Flutter?

We teach you all you need to confidently build apps at hungrimind.com/learn/flutter

ValueNotifier is an implementation of ChangeNotifier. It’s a class that can both add and remove listeners, and these listeners are simple methods.

const counterNotifier = CounterNotifier(); // ValueNotifier
@override
Widget build(BuildContext context) {
return ValueListenableBuilder (
listenable: counterNotifier,
builder: (context, child) {
return Text('${counterNotifier.count}');
}
);
}

So what does that mean?

ValueListenableBuilder

A ValueListenableBuilder takes a ValueNotifier as an argument. The ValueListenableBuilder is a StatefulWidget that adds and removes a listener in initState and dispose.

The callback or method it adds is just a method that does setState.

So anytime the value changes in the ValueNotifier, that will trigger a setState for the builder to make sure the UI rebuilds.

const counterNotifier = CounterNotifier(); // ValueNotifier
@override
Widget build(BuildContext context) {
return ValueListenableBuilder (
listenable: counterNotifier, // adds a listener for setState
builder: (context, child) {
}
);
}

Let’s look at this from a practical standpoint.

The ValueListenableBuilder will take the listenable, in this case, a counterNotifier. Since this ValueListenableBuilder is just a StatefulWidget under the hood, it will add a listener method in the initState (which will call setState).

The listener is notified to be called once the counterNotifier changes its value.

Now, everything inside the builder method will be rebuilt to display the updated value.

const counterNotifier = CounterNotifier(); // ValueNotifier
@override
Widget build(BuildContext context) {
return ValueListenableBuilder (
listenable: counterNotifier, // adds a listener for setState
builder: (context, child) {
// if notifier changes, setState called, builder is rebuilt
return Text('${counterNotifier.count}');
}
);
}

That’s all a ValueNotifier is. You can make your own ValueNotifier by passing a listener method in the initState of a StatefulWidget, but ValueNotifier makes the syntax a little more pleasant.

If you want to learn how to build production-ready Flutter apps, check out our course below.

Course thumbnail

Want to learn Flutter?

We teach you all you need to confidently build apps at hungrimind.com/learn/flutter

Thank you for reading!

YouTube Video

Get articles right in your inbox

No spam, unsubscribe anytime. We treat your inbox with respect.