skip to content

In Flutter's architecture guide, what does the full Command class do on execute(), and how do running, error, completed, result and clearResult() fit together?

level: middleimportance: should knowfreq 33%

answer

  1. guard, then running = true
  2. result cleared and listeners notified
  3. action returns a Result
  4. error and completed read the result
  5. clearResult after the UI reacts

basics

~20 s

execute() returns if already running, otherwise sets running, clears the last result, notifies, awaits the action's Result, then resets running and notifies. error and completed are derived from whether that Result is Error or Ok; clearResult() resets it.

solid answer

~40 s

The full `Command<T>` in the guide's Compass sample is an abstract `ChangeNotifier` with two subclasses, `Command0<T>` and `Command1<T, A>`. `execute` delegates to a private routine that: returns if `running`; sets `running = true` and `result = null`; notifies; awaits the action, which must return `Future<Result<T>>`; and in a `finally` sets `running = false` and notifies again. `error` is `result is Error`, `completed` is `result is Ok`, and `result` holds the last `Result`, so the view can read the value or the exception. Views that react once, such as showing a `SnackBar` on failure, listen with `addListener` and call `clearResult()` after handling it, so the next notification does not repeat the message.

code

dart · 38 lines
dart
import 'package:flutter/material.dart';

import 'editor_view_model.dart'; // exposes saveDraft: Command1<void, Draft>

class EditorScreen extends StatefulWidget {
  const EditorScreen({super.key, required this.viewModel});
  final EditorViewModel viewModel;

  @override
  State<EditorScreen> createState() => _EditorScreenState();
}

class _EditorScreenState extends State<EditorScreen> {
  @override
  void initState() {
    super.initState();
    widget.viewModel.saveDraft.addListener(_onSaveChanged);
  }

  @override
  void dispose() {
    widget.viewModel.saveDraft.removeListener(_onSaveChanged);
    super.dispose();
  }

  void _onSaveChanged() {
    final save = widget.viewModel.saveDraft;
    if (save.error) {
      save.clearResult(); // react once
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Draft not saved. Check your connection.')),
      );
    }
  }

  @override
  Widget build(BuildContext context) => const Placeholder();
}

go deeper

for a junior

Remember the order: guard, running true, action, running false, with a notification at start and end.

for a middle

Explain how error and completed derive from the stored Result, and why clearResult is needed for one-shot UI reactions.

for a senior

Spot the failure paths: actions that throw, stale completed flags, listeners never removed.

for a principal

Choose between the guide's Command, a package such as command_it, and a state library's equivalent, and standardize one-shot event handling.

## Two versions in the guide Flutter's architecture guide shows the command in two forms, and they behave differently on failure: | | Simplified `Command` | Full `Command0` / `Command1` | |---|---|---| | Action type | `Future<void> Function()` | returns `Future<Result<T>>` | | `error` | `Exception?` caught from the action | `bool`: `result is Error` | | Failure handling | `on Exception catch` inside `execute` | the action returns an `Error` result | | Reset | `clear()` | `clearResult()`, which notifies | | Last value | none | `result` | Interviews usually mean the **full** version from the Compass sample app, which the rest of this answer describes. ## Step by step through execute() `Command0.execute()` and `Command1.execute(argument)` both call one private routine in the abstract `Command<T>`: 1. **Guard.** If `running` is already true, return. This is what swallows double taps. 2. **Start.** Set `running = true`, set the stored result to `null`, call `notifyListeners()`. The button can now show a spinner. 3. **Run.** Await the action. It is typed to return `Future<Result<T>>`, so success comes back as `Ok` and failure as `Error`. 4. **Finish.** In a `finally` block, set `running = false` and notify again, whether the action succeeded or not. The getters then read the stored result: - `running`: the flag set in steps 2 and 4; - `error`: `true` when the last result is an `Error`; - `completed`: `true` when the last result is an `Ok`; - `result`: the last `Result<T>` itself, or `null` before the first run and while running. The guide's doc comment says `result` is null after an error, but the code stores the `Error` result, so `result` is non-null and carries the exception. ## Reacting once: listeners and clearResult Some reactions must happen **once**, not on every rebuild: a `SnackBar` saying 'Draft not saved, you are offline', or navigating away after publishing. The guide's approach in a `StatefulWidget`: 1. In `initState`, `viewModel.saveDraft.addListener(_onSaveChanged)`. 2. In `_onSaveChanged`, if `saveDraft.error` is true, call `saveDraft.clearResult()` and show the `SnackBar`. 3. In `dispose`, remove the listener. Without `clearResult()`, the command still reports `error` on its next notification, and the same message appears again. `clearResult()` itself notifies, so builders that show an inline error also clear. ## A blogging app's editor ```dart class EditorViewModel extends ChangeNotifier { EditorViewModel({required DraftRepository drafts}) : _drafts = drafts { saveDraft = Command1<void, Draft>(_saveDraft); } final DraftRepository _drafts; late final Command1<void, Draft> saveDraft; Future<Result<void>> _saveDraft(Draft draft) => _drafts.save(draft); } ``` When the device is offline, the repository returns `Result.error(...)`; the command's `error` turns true, the listener shows the `SnackBar` once, and the button leaves its spinner state because `running` went back to false in `finally`. ## What to watch for - An action that **throws** instead of returning `Error` escapes the `finally`: `running` resets but `result` stays `null`, so neither `error` nor `completed` is true. - Reading `completed` for 'saved' after a later run started: step 2 cleared the result, so it is false again while running. - Forgetting `removeListener` in `dispose`, which leaves a callback pointing at an unmounted widget.

  • Why does clearResult() call notifyListeners() but the simplified clear() does not?
    In the full version, builders may display the last result, such as an inline error, so clearing it must trigger a rebuild. The simplified demo's `clear()` only resets fields and relies on the next state change to notify.
  • Why does the full Command require actions to return Result instead of catching exceptions?
    It makes failure part of the action's type. The repository converts exceptions into `Error` at the data-layer boundary, and the command only inspects which subclass came back, so the UI never depends on an unchecked exception reaching it.

saying these in an interview costs you the question

  • The full Command catches any exception thrown by the action.
  • result is always null after a failed run.
  • completed stays true while the command is running again.
  • clearResult() is optional because error resets itself on the next tap.
  • error and completed can both be true after one run.