Skip to main content

Observer Pattern: The Hidden Magic Behind Flutter State Management

When I first started learning state management in Flutter, it honestly felt like magic. I'd change one variable, call setState , and the screen would just… update. Later I met ValueNotifier and ValueListenableBuilder , and it got even stranger — now only one widget would rebuild while everything around it stayed perfectly still. I hadn't written any code to watch for changes. I hadn't wired anything up. It just worked. And anything that "just works" without me understanding why makes me a little uncomfortable. So I did what any curious developer does: I went down the rabbit hole. I wanted to know how a value "knows" when to tell the UI to rebuild. Who is listening? How does the change travel from my variable to the pixels on screen? That question led me to something much bigger than Flutter — a classic software design pattern called the Observer pattern . Once I understood it, the magic didn't disappear. It just turned into some...

Observer Pattern: The Hidden Magic Behind Flutter State Management


When I first started learning state management in Flutter, it honestly felt like magic.

I'd change one variable, call setState, and the screen would just… update. Later I met ValueNotifier and ValueListenableBuilder, and it got even stranger — now only one widget would rebuild while everything around it stayed perfectly still. I hadn't written any code to watch for changes. I hadn't wired anything up. It just worked. And anything that "just works" without me understanding why makes me a little uncomfortable.

So I did what any curious developer does: I went down the rabbit hole. I wanted to know how a value "knows" when to tell the UI to rebuild. Who is listening? How does the change travel from my variable to the pixels on screen?

That question led me to something much bigger than Flutter — a classic software design pattern called the Observer pattern. Once I understood it, the magic didn't disappear. It just turned into something better: a mechanism I could actually reason about. This post is the explanation I wish I'd had at the start.

What is the Observer design pattern?

The Observer pattern is one of the classic "Gang of Four" behavioral design patterns. It solves a single, very common problem:

How does one object tell a group of other objects that something changed — without knowing or caring who those objects are?

That last part is the important bit. The object that holds the data does not know about the objects that react to it. It doesn't import them, reference them by name, or understand what they do. It just keeps a list of "things that want to be told," and when something changes, it tells them all. This is called decoupling, and it's the reason the pattern is so powerful.

There are only two roles in the whole pattern:

  1. The Subject (or Observable) holds the state, keeps a private list of who's watching, and notifies each watcher when it changes.
  2. The Observers (or Subscribers) each register interest with the Subject, then wait. When the Subject fires, they react.

And there are only three actions: subscribe (an observer joins the list), notify (the subject calls everyone), and unsubscribe (an observer leaves the list). That's the entire pattern — everything else is bookkeeping.

A diagram of the observer pattern: a central SUBJECT box labelled ValueNotifier sends notifyListeners() arrows out to three OBSERVER boxes, with a dashed addListener/subscribe arrow pointing back.
The subject holds a list of observers and calls them all when its value changes.

How it works

The mechanics are simpler than they sound. The Subject holds an internal list — think of it as a guest list of callbacks. When an Observer subscribes, it hands the Subject a function: "call this when you change." The Subject just stores it.

Eventually the Subject's state changes. At that moment it loops through its list and calls every function on it, one by one. Each call is a notification; the Observers wake up and react. When an Observer no longer cares, it unsubscribes and the Subject drops its function from the list — a step that matters, because a Subject still calling a dead Observer is a memory leak.

Three boxes in a row: 1 SUBSCRIBE (addListener, observer joins the list), 2 NOTIFY (notifyListeners, loop the list and call each), 3 UNSUBSCRIBE (removeListener, observer leaves the list), with a loop arrow over NOTIFY reading repeats on every change.
Subscribe once, get notified on every change, unsubscribe when you're done.

Notice that the Subject never asks "who are you?" or "what will you do?" It just calls whatever is on the list. That indifference is the feature, not a limitation.

The example that made it click: ValueNotifier

Here's the part that turned the abstract pattern into something real for me. Flutter's ValueNotifier is the Observer pattern with the lid off. It's a Subject that holds exactly one value, and it extends ChangeNotifier, where the actual list-of-listeners machinery lives. Here's a simplified version of both — essentially the whole pattern in a dozen lines of Dart:

value_notifier.dart
class ChangeNotifier {
  final List<VoidCallback> _listeners = [];

  void addListener(VoidCallback listener) => _listeners.add(listener);
  void removeListener(VoidCallback listener) => _listeners.remove(listener);

  void notifyListeners() {
    for (final listener in _listeners) {
      listener(); // call every observer on the list
    }
  }
}

class ValueNotifier<T> extends ChangeNotifier {
  ValueNotifier(this._value);

  T _value;
  T get value => _value;

  set value(T newValue) {
    if (_value == newValue) return; // skip if nothing actually changed
    _value = newValue;
    notifyListeners();              // broadcast to every observer
  }
}

Look at the value setter — that's the heart of it. Assigning a genuinely different value (checked with ==) calls notifyListeners(), which loops the list and calls each listener. The _listeners list is the guest list; addListener is subscribe. The magic was just a for loop over a list of functions.

Seeing the three verbs, without any UI

Before bringing widgets in, here's the raw cycle with all three actions in one place:

demo.dart
final counter = ValueNotifier<int>(0);

void listener() => print('Value is now ${counter.value}');

counter.addListener(listener);   // subscribe
counter.value = 1;               // prints "Value is now 1"
counter.value = 1;               // nothing — the value didn't change
counter.value = 2;               // prints "Value is now 2"

counter.removeListener(listener); // unsubscribe
counter.dispose();                // clean up

Where the rebuild "magic" actually happens

In a real app you don't call addListener yourself — you hand your notifier to a ValueListenableBuilder, and it subscribes for you:

counter_page.dart
final ValueNotifier<int> _counter = ValueNotifier<int>(0);

// Only this Text rebuilds when the value changes.
ValueListenableBuilder<int>(
  valueListenable: _counter,
  builder: (context, value, child) {
    return Text('Count: $value');
  },
)

FloatingActionButton(
  onPressed: () => _counter.value++, // triggers notifyListeners
  child: const Icon(Icons.add),
)

Here's what finally dissolved the mystery. ValueListenableBuilder is just a small widget that acts as an Observer. When it's created it subscribes; its callback calls setState, which rebuilds that widget; when it leaves the screen it unsubscribes. In simplified form:

builder_state.dart
void initState() {
  super.initState();
  widget.valueListenable.addListener(_onChange); // subscribe
}

void _onChange() {
  setState(() {}); // the notification → schedule a rebuild
}

void dispose() {
  widget.valueListenable.removeListener(_onChange); // unsubscribe
  super.dispose();
}

So the full journey of a single button tap looks like this:

A flow diagram of five boxes connected by arrows: _counter.value++ (you mutate the subject), notifyListeners() (subject calls the list), _onChange() (observer's callback runs), setState() (mark widget dirty), rebuild (only this widget).
The value never reaches out to the UI — the UI subscribed to the value.

And the reason only that one Text rebuilds — not the whole screen — is that the setState belongs to the little builder widget, so Flutter rebuilds from there down. That was the exact "how does it know?" question I started with, finally answered.

Looking back

The thing I love about this journey is that the magic never actually goes away — it just relocates. What felt like Flutter secretly watching my variables turned out to be a decades-old design pattern doing something almost boring: keeping a list of functions and calling them in a loop. But that "boring" mechanism is exactly what lets you control rebuilds with surgical precision.

Once you can see the Subject, the Observers, and the subscribe–notify–unsubscribe cycle underneath ValueNotifier, the rest of Flutter's reactivity — providers, animation controllers, streams — stops looking like magic and starts looking like the same simple idea wearing different clothes.

The takeaway

If you're at the "this feels like magic" stage right now, my advice is: chase the feeling. Go read the source of the thing that's confusing you. More often than not, there's a clean, understandable pattern waiting underneath — and understanding it will make you a noticeably better developer.

Comments

Popular posts from this blog

Keep Your Promises: The Liskov Substitution Principle Made Simple

If you're learning clean code, you'll keep running into the "SOLID" principles. The L stands for the Liskov Substitution Principle (LSP) — a scary name for a genuinely simple idea. This guide starts from zero, so no prior knowledge is needed. What it is The Liskov Substitution Principle says: If something is supposed to work like a certain type, then any "version" of that type must be able to take its place without causing problems. That's the whole thing. If you call something a Doctor , it has to actually behave like a doctor everywhere a doctor is expected. If you call something an ApiClient , it has to behave like one everywhere. Break that, and your program fails — usually later, in a confusing way. The principle is named after Barbara Liskov, a computer scientist who described it back in 1987. But you don't need the academic version to use it well. First, what does "substitute" mean? This is the word that makes the whole pr...

Understanding BuildContext in Flutter!

As a Flutter developer, you might have encountered the term BuildContext early in your journey. Despite its importance, many developers struggle to grasp its full significance. Today, we’re going to dive into what BuildContext is, what it does, and why it’s crucial for running your Flutter app. What is BuildContext In both stateless and stateful widgets, when you override the build method, it takes BuildContext context as a parameter. This context is provided by the Flutter framework. Here’s a simple example to illustrate: Widget build(BuildContext context) { return OutlinedButton( onPressed: () async { await Future< void >.delayed( const Duration(seconds: 1 )); if (context.mounted) { Navigator.of(context).pop(); } }, child: const Text( 'Delayed pop' ), ); } BuildContext do a simple task, track all of your widget location. In flutter, it all about widgets. Everything is build on a collection of widgets. Some widgets are parent w...

Impeller: Elevating Flutter Performance with Predictable Rendering

In the world of Flutter, Impeller is a buzzword that has caught the attention of developers and enthusiasts alike. But what exactly is Impeller, and why is it crucial for Flutter apps? Let’s dive into the details. What is Impeller? Impeller is a new rendering engine for flutter. Now, many of you can think. What is a rendering engine. Rendering engine is nothing but a piece of software responsible for converting input instructions or data into visual or audible output. Basically , it helps us to draw UI of an app according to the instruction you give it. Flutter engine is written is C/C++language. Which is the part of dart UI. Rendering engine in Flutter orchestrates the transformation from widget tree to pixels on the screen. Before Impeller flutter used skia as a default graphics engine. But, there was some issues on it. Challenges with Skia: Skia has been the graphic engine for Flutter since its inception. Skia is also a powerful render engine, which powers various platforms, inc...