Irbisa · Cheatsheet

Modelling App State

Immutability, freezed, sealed unions, equality, optimistic updates, state restoration

Middle Developer · 19 questions

  1. 01

    What is the difference between ephemeral (local) state and app state in Flutter, and how do you decide where a value lives?

    Ephemeral state belongs to one widget and dies with it (a field's focus, the selected tab, an animation).

  2. 02

    What does immutable state mean, and why do Cubit and Notifier expect a new state object on every change?

    Immutable state is never changed in place: every update builds a new object, usually with copyWith.

    class CartState {  const CartState({this.items = const []});  final List<Item> items;   CartState copyWith({List<Item>? items}) =>      CartState(items: items ?? this.items);…
  3. 03

    How do you model loading, data and error state: separate booleans or a sealed class?

    A sealed class with one subtype per state, each carrying only its own data.

    sealed class FeedState {  const FeedState();} final class FeedLoading extends FeedState {  const FeedLoading();…
  4. 04

    Find the bug: after offline caching shipped, users with a stale cache see an empty orders screen.

    // OrdersState is sealed: OrdersLoading, OrdersLoaded, OrdersFailed, OrdersStale@overrideWidget build(BuildContext context) {  return switch (context.watch<OrdersCubit>().state) {    OrdersLoading() => const Center(child: CircularProgressIndicator()),    OrdersLoaded(:final items) => OrderList(items),    OrdersFailed(:final message) => ErrorView(message),    _ => const SizedBox.shrink(),  };}

    The wildcard _ catches every state the switch does not name, so OrdersStale falls into SizedBox.shrink() and renders nothing.

  5. 05

    This widget sorts the list it gets from the cubit before building it. What goes wrong, and why did final not stop it?

    class FeedState {  const FeedState(this.items);  final List<Item> items;} @overrideWidget build(BuildContext context) {  final state = context.watch<FeedCubit>().state;  state.items.sort((a, b) => b.date.compareTo(a.date));  return ListView(    children: [for (final item in state.items) ItemTile(item)],  );}

    sort() reorders the cubit's own list in place, so the emitted state changes behind the cubit's back and everyone holding it sees the new order.

  6. 06

    How do you implement value equality for a state class, and why does it matter for rebuilds?

    Override == and hashCode so two states with the same field values are equal (by hand, with Equatable, or generated by freezed).

    class FeedState {  const FeedState({this.items = const [], this.loading = false});  final List<Item> items;  final bool loading;   @override…
  7. 07

    The cubit sets onlyInStock on every tap of the switch, but the list stops reacting. What is wrong with this state class?

    class FilterState extends Equatable {  const FilterState({this.query = '', this.onlyInStock = false});  final String query;  final bool onlyInStock;   FilterState copyWith({String? query, bool? onlyInStock}) => FilterState(        query: query ?? this.query,        onlyInStock: onlyInStock ?? this.onlyInStock,      );   @override  List<Object?> get props => [query];}

    onlyInStock is missing from props, so a state that differs only in that flag is == to the current one.

  8. 08

    Why do hand-written copyWith methods break down in nested state, and what does freezed give you instead?

    Every level has to list every field, so a nested update becomes a chain of copyWith calls where one forgotten field silently keeps its old value, and passing null cannot clear anything.

    // wrong: hand-written, three levels deep, and null cannot clear a fieldfinal next = state.copyWith(  user: state.user.copyWith(    address: state.user.address.copyWith(city: 'Almaty'),  ),);…
  9. 09

    Find the bug: users who leave the profile screen while it is still loading trigger a crash report "Bad state: Cannot emit new states after calling close".

    class ProfileCubit extends Cubit<ProfileState> {  ProfileCubit(this._api) : super(ProfileLoading());  final ProfileApi _api;   Future<void> load() async {    final profile = await _api.fetch();    emit(ProfileLoaded(profile));  }}

    load() awaits the network and then calls emit, but by the time the response arrives BlocProvider may have closed the cubit because the screen was popped.

  10. 10

    Why store total as a getter instead of a field, and what goes wrong when a screen copies state in initState?

    Derived values are computed from the state, never stored, and a screen reads the state it shows instead of copying it.

  11. 11

    When should related fields live in one state object instead of several separate notifiers?

    When they change together or must stay consistent with each other.

    // wrong: three notifiers, three notificationsvoid applyFilter(Filter f) {  filter.value = f;          // a listener here still sees page 4  page.value = 1;            // and here the old selection  selected.value = const {}; // only now consistent}…
  12. 12

    Why would you normalize state into a map keyed by id instead of nesting the same objects inside several lists?

    So each entity is stored once and every list holds only ids.

    class FeedState {  const FeedState({this.posts = const {}, this.feed = const [], this.saved = const []});   final Map<PostId, Post> posts; // each post stored once  final List<PostId> feed;       // lists hold only ids  final List<PostId> saved;…
  13. 13

    How do you model the state of a multi-step form, and when does the half-filled data become a real domain object?

    Keep one draft object owned by the whole flow, with nullable, unvalidated fields, and turn it into the validated domain object once, on submit.

    class SignupDraft {  String? email;  String? name;   // null while a required field is still missing  Signup? toSignup() {…
  14. 14

    How do you implement an optimistic UI update and roll back when the request fails?

    Apply the change to the state at once, mark the item as pending, send the request, and on failure undo exactly that change and tell the user.

    enum Sync { synced, pending, failed } class FeedCubit extends Cubit<List<Post>> {  FeedCubit(this._api) : super(const []);  final Api _api;…
  15. 15

    A user taps the star twice quickly. What can this cubit end up showing, and how do you make the result deterministic?

    class FavoriteCubit extends Cubit<bool> {  FavoriteCubit(this._api, bool initial) : super(initial);  final Api _api;   Future<void> toggle() async {    final next = !state;    await _api.setFavorite(next);    emit(next);  }}

    Both taps read state before either reply arrives, so both send true and the star stays on after two taps.

  16. 16

    How do you decide how long a state holder lives (widget, route, session or app), and what goes wrong when it lives too long?

    Give a holder the shortest lifetime that all its readers share, and let whoever creates it dispose it.

  17. 17

    How does state restoration work in Flutter with RestorationMixin, and what do you restore versus re-fetch after the OS kills the app?

    Set restorationScopeId on MaterialApp, mix RestorationMixin into a State and register Restorable* properties in restoreState; the OS keeps them and hands them back after process death.

    class _CheckoutPageState extends State<CheckoutPage> with RestorationMixin {  final _note = RestorableTextEditingController();  final _orderId = RestorableStringN(null);   @override  String? get restorationId => 'checkout';…
  18. 18

    How would you model a checkout flow as a state machine, and what does a pure transition function buy you?

    Make each step a subtype of a sealed state class, each input a subtype of a sealed event class, and write one synchronous next(state, event) that returns the new state.

    sealed class CheckoutState {}final class Idle extends CheckoutState {}final class Submitting extends CheckoutState {}final class AwaitingConfirmation extends CheckoutState {  AwaitingConfirmation(this.paymentId);  final String paymentId;…
  19. 19

    How do you version persisted state so that an app update cannot crash on data written by an older version?

    Save a version number with the payload, migrate step by step from any old version to the current one, and if anything fails to parse, delete the stored data and start from the initial state instead of crashing.

    const _version = 3; final _migrations = <int, Map<String, dynamic> Function(Map<String, dynamic>)>{  1: (json) => {...json, 'plan': 'free'},          // v1 -> v2: new field  2: (json) => {                                   // v2 -> v3: renamed field        ...json,…