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?

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
@overrideWidget 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
@overrideWidget 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
@overrideWidget 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.

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.