In flutter_redux, when do StoreConnector's onInit, onWillChange, onDidChange and onInitialBuild run, and which one should trigger navigation?
answer
- onInit in initState, before the first view model
- onWillChange before the rebuild
- onDidChange after the frame
- onInitialBuild after the first frame
- navigate from onWillChange
basics
~20 sonInit runs in initState before the first view model is built, onWillChange runs on a change before the rebuild, onDidChange and onInitialBuild run after the frame via post-frame callbacks, and onDispose runs in dispose. Navigation belongs in onWillChange.
solid answer
~40 sflutter_redux's `StoreConnector` exposes lifecycle hooks around its internal `State`. `onInit(store)` runs in `initState` **before** the first `converter` call, so it is the place to dispatch a "load" action — the first build already sees its result if the reducer handles it synchronously. `onWillChange` runs for each accepted change before the builder is called; since 0.6.0 it receives the previous and the new view model, which suits imperative reactions such as pushing a receipt route when a payment turns successful. `onDidChange` runs after the frame through a post-frame callback, and `onInitialBuild` once after the first build with the initial view model, for things like a snackbar. `onDispose(store)` runs in `dispose`. With `distinct: true`, the change hooks fire only when the view model really changed. The library's own docs warn against navigating from `onDidChange`.
code
dart · 17 linesimport 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
class PosState {
const PosState(this.offline);
final bool offline;
}
Widget offlineBanner() => StoreConnector<PosState, bool>(
distinct: true,
converter: (store) => store.state.offline,
onInitialBuild: (offline) {
if (offline) debugPrint('Till started offline');
},
builder: (context, offline) =>
offline ? const Text('Offline: card payments queued') : const SizedBox.shrink(),
);go deeper
Remember onInit for loading on first display and onDispose for clean-up, both receiving the store.
Explain the order: onInit before the first converter call, onWillChange before a rebuild, onDidChange and onInitialBuild after the frame, and how distinct gates them.
Use onWillChange's previous and new view models to react to transitions such as a payment approval, and avoid navigation from post-frame hooks.
Decide whether imperative reactions belong in widget callbacks at all, or in middleware and a navigation service, so flows stay testable without a widget tree.
## The hooks and where they run `StoreConnector` builds a private `StatefulWidget` that listens to the store. Its callbacks map onto that `State`'s lifecycle: | Callback | Receives | Runs | Typical use | |---|---|---|---| | `onInit` | the `Store` | in `initState`, before the first `converter` call | dispatch "load the open orders" | | `onInitialBuild` | the first view model | after the first build, via a post-frame callback | show a snackbar once | | `onWillChange` | previous and new view model | on each accepted change, before the builder runs | navigate, drive a `TabController` | | `onDidChange` | the new view model | after the rebuilt frame, via a post-frame callback | start an animation | | `onDispose` | the `Store` | in `dispose` | dispatch "clear stale data" | ## onInit: dispatch before the first view model Because `onInit` runs before the converter produces the first view model, an action it dispatches is already reflected in the first build when the reducer handles it synchronously — flutter_redux's tests check exactly that. In the point-of-sale app, the open-orders screen dispatches `LoadOpenOrders()` from `onInit`; middleware starts the fetch, and a reducer sets a loading flag the first frame can show. ## onWillChange: react before the rebuild For each change that passes `ignoreChange` and `distinct`, the connector calls `onWillChange(previous, next)` and then emits the new view model to its builder. - **0.6.0 made this a breaking change**: it now receives both the previous and the new view model, so you can react to transitions ("payment went from pending to approved") instead of to states. - flutter_redux's docs recommend it for imperative calls such as `Navigator` and `TabController`. ## onDidChange and onInitialBuild: after the frame Both are scheduled with `WidgetsBinding.instance.addPostFrameCallback`, so they run once the frame containing the new build has been laid out: - `onInitialBuild` runs once, with the view model from the first build. - `onDidChange` runs after each accepted change. The library documents that using a `BuildContext` inside `onDidChange` to navigate can cause problems and points to `onWillChange` for navigation. ## How distinct gates the change hooks With `distinct: false` (the default) every store change calls `onWillChange` and `onDidChange`, even when nothing this widget shows changed. Navigation reacting to a transition would then need its own comparison. With `distinct: true` and a value-equal view model, the hooks fire only for real changes — one more reason to give view models proper `==`. ## A payment flow, end to end 1. The checkout screen's connector converts the store into `CheckoutVm(status: ...)` with `distinct: true`. 2. The cashier taps "Charge"; middleware talks to the terminal and dispatches `PaymentApproved`. 3. The reducer sets `status` to approved, and the store emits. 4. `onWillChange(previous, next)` sees the transition from pending to approved and pushes the receipt route. 5. The builder redraws the checkout screen underneath. ```dart import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; enum PayStatus { idle, pending, approved } class CheckoutVm { const CheckoutVm(this.status); final PayStatus status; @override bool operator ==(Object other) => other is CheckoutVm && other.status == status; @override int get hashCode => status.hashCode; } class PosState { const PosState(this.status); final PayStatus status; } class LoadOpenOrders {} final navigatorKey = GlobalKey<NavigatorState>(); Widget checkout() => StoreConnector<PosState, CheckoutVm>( distinct: true, onInit: (store) => store.dispatch(LoadOpenOrders()), converter: (store) => CheckoutVm(store.state.status), onWillChange: (previous, next) { if (previous?.status != PayStatus.approved && next.status == PayStatus.approved) { navigatorKey.currentState?.pushNamed('/receipt'); } }, builder: (context, vm) => Text('Status: ${vm.status.name}'), ); ``` The `previous?.status` guard is written for the later, null-safe flutter_redux releases, where the previous view model is nullable; in the pinned 0.6.0 source it is simply the last view model.
- Why is onInit a better place than build to dispatch a load action?`build` can run many times — on every parent rebuild and every store change — so dispatching there repeats the load and can loop, because the dispatch itself triggers another rebuild. `onInit` runs once in `initState`, before the first view model is built.
- What changed about onWillChange in flutter_redux 0.6.0, and why does it matter?It became a breaking change: the callback now receives the previous view model and the new one instead of only the new one. That lets code react to a transition — for example, navigating only when a payment moves from pending to approved — without storing the last value itself.
saying these in an interview costs you the question
- onInit runs after the first build, so the first frame never reflects its dispatch.
- onDidChange is the recommended place to call Navigator.
- onWillChange receives only the new view model in 0.6.0.
- With distinct: false, onWillChange fires only when the view model changes.
- Dispatching a load action inside the builder is equivalent to onInit.