In Riverpod 3, how does a Notifier's build() method work, and why must initialization live in build rather than the constructor?
answer
- build returns the initial state
- no ref in the constructor
- re-runs when a watched provider changes
- same instance across rebuilds
- state setter notifies unless ==
basics
~20 sbuild() 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 sA `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
Remember that build() returns the initial state and that methods change it by assigning state.
Explain the create-attach-build sequence, why the constructor has no ref, and how == filtering decides whether listeners are notified.
Reason about what a dependency change does to state set by methods, and design notifiers so user choices survive rebuilds.
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.