skip to content

In a Flutter view model following the architecture guide, how would you make 'save draft' optimistic, and how do you roll back safely when the save fails offline?

level: seniorimportance: should knowfreq 35%

answer

  1. show success before the call returns
  2. snapshot, update, notify
  3. await the repository Result
  4. restore the snapshot on Error
  5. one-shot error, then clear

basics

~20 s

Update the UI state as if the save succeeded and notify, then await the repository. On an Error result, restore the state captured before the change, flag the error for a one-shot message, and notify again.

solid answer

~40 s

Optimistic state, as the guide's recipe describes it, shows the success state immediately and reverts only on failure. For 'save draft': snapshot the current state, set it to 'Saved just now' (and add the draft to the drafts list), `notifyListeners()`, then await `DraftRepository.save`. On `Ok`, keep it. On `Error`, restore the **snapshot**, not a guessed value, set an error flag and notify; the view's listener shows a `SnackBar` once and clears the flag. Two traps: a second save started while the first is in flight, which the guide's `Command` blocks by ignoring `execute` while running, and a rollback that overwrites edits made after the snapshot, which you avoid by rolling back only the fields the save changed or by checking a version counter.

code

dart · 42 lines
dart
import 'package:flutter/foundation.dart';

import 'result.dart'; // the guide's sealed Result<T>

class Draft {
  const Draft(this.id, this.body);
  final String id;
  final String body;
}

abstract class DraftRepository {
  Future<Result<void>> save(Draft draft);
}

class EditorViewModel extends ChangeNotifier {
  EditorViewModel({required DraftRepository drafts}) : _drafts = drafts;
  final DraftRepository _drafts;

  DateTime? lastSavedAt;
  bool saveFailed = false;
  bool _saving = false;

  Future<void> saveDraft(Draft draft) async {
    if (_saving) return; // no overlapping saves
    _saving = true;

    final previousSavedAt = lastSavedAt; // snapshot what this save changes
    lastSavedAt = DateTime.now(); // optimistic
    notifyListeners();

    final result = await _drafts.save(draft);
    switch (result) {
      case Ok<void>():
        break; // UI already shows success
      case Error<void>():
        lastSavedAt = previousSavedAt; // restore, don't guess
        saveFailed = true; // view shows a SnackBar once, then resets it
    }
    _saving = false;
    notifyListeners();
  }
}

go deeper

for a junior

Describe the idea: show success immediately, undo if the save fails.

for a middle

Write the five-step method with a snapshot, a Result switch and a one-shot error.

for a senior

Handle overlapping saves and edits made during the save, and judge which actions should not be optimistic.

for a principal

Set product-level rules for optimistic UI, pending indicators and failure messaging across the app.

## What optimistic state is Flutter's architecture guide has a recipe for **optimistic state**, also called optimistic UI: present the successful result **before** the background work finishes, and revert only if it fails. Its example is a 'Subscribe' button that turns into 'Subscribed' the moment it is tapped, while the request is still running. Users perceive the app as faster because nothing waits on the network in the common case. ## The shape of the view model method The guide's recipe follows five steps, adapted here to a blogging app's 'save draft': 1. **Guard.** Ignore the tap if a save is already running. 2. **Apply optimistically.** Record the current state, then set the UI state to the success value, such as `lastSavedAt = now`, and call `notifyListeners()`. 3. **Do the real work.** Await `DraftRepository.save(draft)`. 4. **Keep or revert.** On success, nothing more is needed. On failure, restore the recorded state and set an error flag. 5. **Notify.** Call `notifyListeners()` again so the view reflects the outcome. The recipe itself catches an exception from the repository; with the guide's `Result` type, step 4 becomes a `switch` on `Ok` and `Error`. ## Rolling back safely The recipe's rollback is simple because its state is one boolean. Real screens are messier: | Risk | What goes wrong | Safer approach | |---|---|---| | Guessing the old value | Setting `lastSavedAt = null` erases a previous successful save time | Restore the value captured before the change | | Overlapping saves | Save A fails after save B succeeded; rolling back A erases B | Block re-entry while running, or tag each attempt and ignore stale results | | Edits during the save | Restoring a whole state snapshot discards text typed meanwhile | Roll back only the fields this save changed | | Silent failure | The UI quietly returns to 'unsaved' | Set an error flag the view turns into a one-shot `SnackBar` | The guide's `Command` handles the overlapping case for you: `execute` returns immediately while a run is in progress. ## When not to be optimistic - Actions whose failure is common, such as saving while the app already knows it is offline; show 'Not saved' directly. - Irreversible or costly actions, such as publishing a post or paying, where showing success that later reverts would mislead the user. - Actions whose result the server decides, such as a generated slug or a moderation verdict. The guide suggests a middle ground: a third, **pending** state, like a chat message shown with a 'sending' icon until delivery is confirmed. For drafts, 'Saving...' next to the optimistic text is that state. ## Showing the error once The recipe's view registers a listener in `initState`, checks the error flag in the callback, resets it, and shows a `SnackBar` through `ScaffoldMessenger`; it removes the listener in `dispose`. Resetting the flag before showing the message keeps a later `notifyListeners()` from showing it twice. ## Testing it A view model test drives both branches with a fake repository: one returning `Result.ok(null)` after a delay, one returning `Result.error(SocketException('offline'))`. Assert the state immediately after calling save (optimistic), and again after awaiting it (kept or restored).

  • How would you show that the optimistic save is not yet confirmed?
    Add a pending state, as the guide suggests for chat messages: show 'Saving...' or a small icon next to the optimistic 'Saved' until the repository returns `Ok`, then drop it. On `Error`, restore the previous value and show the message.
  • Why is optimistic UI a poor fit for 'publish post'?
    Publishing is visible to others and hard to take back, and the server may reject it. Showing 'Published' and then reverting misleads the user at the worst moment; a normal loading state is clearer.

saying these in an interview costs you the question

  • Rollback can just reset the field to null or false.
  • Optimistic updates remove the need to handle the error.
  • Every action should be optimistic to make the app feel fast.
  • A second tap during the save should start another optimistic update.
  • The error message should be shown on every notifyListeners call until the next save.