skip to content

In Riverpod 3, how does a Notifier's build() method work, and why must initialization live in build rather than the constructor?

level: middleimportance: must knowfreq 50%

answer

  1. build returns the initial state
  2. no ref in the constructor
  3. re-runs when a watched provider changes
  4. same instance across rebuilds
  5. state setter notifies unless ==

basics

~20 s

build() returns the notifier's initial state and may watch other providers; it re-runs, on the same instance, when one of them changes. The constructor runs before the notifier is attached, so ref and state are unavailable there.

solid answer

~40 s

A `Notifier<T>` is declared with `NotifierProvider<MyNotifier, T>(MyNotifier.new)`. Riverpod creates the instance, attaches it to its provider element, and then calls `build()`, whose return value becomes `state`. `build()` may use `ref.watch`; when a watched provider changes, `build()` runs again and its result replaces the state, while the notifier instance itself is kept. Methods change state by assigning `state = ...`, which notifies listeners only when the new value is not `==` to the old one (overridable through `updateShouldNotify`). Using `ref` or `state` in the constructor throws a `StateError` telling you to move the logic into `build`, because the instance is not attached yet. Widgets call methods through `ref.read(provider.notifier)`; `state` itself is `@protected`, so outside code does not assign it.

go deeper

for a junior

Remember that build() returns the initial state and that methods change it by assigning state.

for a middle

Explain the create-attach-build sequence, why the constructor has no ref, and how == filtering decides whether listeners are notified.

for a senior

Reason about what a dependency change does to state set by methods, and design notifiers so user choices survive rebuilds.

for a principal

Set conventions for what belongs in build() versus methods, and how immutable state types keep notification behaviour predictable across a codebase.

## The shape of a Notifier In **Riverpod 3**, mutable synchronous state is a class extending **`Notifier<T>`**, exposed by a **`NotifierProvider`**: ```dart final notificationFilterProvider = NotifierProvider<NotificationFilterNotifier, NotificationFilter>( NotificationFilterNotifier.new, ); class NotificationFilterNotifier extends Notifier<NotificationFilter> { @override NotificationFilter build() { final prefs = ref.watch(notificationPrefsProvider); return prefs.defaultFilter; } void showMentionsOnly() => state = NotificationFilter.mentionsOnly; } ``` The provider receives a function that creates the notifier - usually a constructor tear-off such as `NotificationFilterNotifier.new` - not a function receiving `ref`. The notifier gets its `ref` as a property. ## What happens when it is first read 1. A widget watches `notificationFilterProvider`. 2. Riverpod calls the create function and attaches the new notifier to the provider's element. 3. It calls **`build()`**. The returned value becomes `state`. 4. Watchers receive that state. Step 2 is why the **constructor must stay empty of logic**. Until the notifier is attached, it has no `ref` and no `state`; touching either throws a `StateError` whose message says you tried to use ref or state inside the constructor and should move the logic into `build`. ## When build() runs again `build()` is reactive. Everything it `ref.watch`es becomes a dependency: - when a watched provider changes, Riverpod runs `build()` again and its result **replaces** the current state - changes made through methods are discarded; - the source documentation states that the notifier is **not recreated**: the same instance is kept between executions of `build()`; - when the provider is disposed (for example an auto-dispose provider with no listeners) and later read again, a fresh instance is created and `build()` starts from scratch. This replaces the older `StateNotifier` model, where a changed dependency disposed and recreated the whole notifier. ## Changing state - Assign **`state = newValue`** inside a method. Reading `state` in methods returns the current value. - Treat state as immutable: build a new value (`copyWith`, a new list) rather than mutating the old one. - Listeners are notified when `updateShouldNotify(previous, next)` returns true; the default is `previous != next`, so an equal value is silently ignored. Override it (for example with `identical`) only for a specific reason. - `state` is `@protected`: widgets do not assign it directly. They call methods through `ref.read(notificationFilterProvider.notifier).showMentionsOnly()`. | Where | May use `ref`? | May use `state`? | |---|---|---| | constructor | no - throws | no - throws | | `build()` | yes, including `ref.watch` | only after it has been assigned | | methods | yes (`ref.read` for one-off reads) | yes | ## Pitfalls - **Logic in the constructor.** Fails at runtime with the uninitialized-notifier error. - **Reading `state` early in `build()`.** In a synchronous `Notifier`, reading `state` inside `build()` before it has a value can throw; return the initial value instead. - **Expecting method changes to survive a dependency change.** The rebuild recomputes state from `build()`. - **Mutating a list in place.** The object stays `==` to itself, so no listener is notified. - **A throwing `build()`.** Reading a synchronous notifier whose `build()` threw rethrows the error; use `AsyncNotifier` if loading can fail and should become a state.

  • A method set state to mentionsOnly, then a provider that build() watches changed. What is the state now?
    Whatever `build()` returns on its re-run - the method's change is replaced. The instance is kept, but its state is recomputed from `build()`. If a user choice must survive dependency changes, store it somewhere `build()` reads, such as another provider or persisted settings.
  • Why does state = [...state, item] notify listeners while state.add(item) does not?
    Riverpod notifies when the new state is not `==` to the previous one. Assigning a new list produces a different value; mutating the existing list in place leaves `state` pointing at the same object, which is equal to itself, and in-place mutation is not even an assignment. Build new values instead of mutating.

saying these in an interview costs you the question

  • Reads ref inside the Notifier's constructor to load initial data.
  • Believes a dependency change creates a brand-new notifier instance in Riverpod 3.
  • Expects state changed by a method to survive build() re-running.
  • Assigns notifier.state directly from widget code.
  • Mutates a list inside state in place and expects a rebuild.