In Flutter's architecture guide, how does a view use a view model's Command to show a spinner and ignore double taps while 'save draft' runs?
answer
- a Command is a ChangeNotifier
- listen to the command, not the view model
- running drives the spinner
- execute returns early while running
- onPressed wired to execute
basics
~10 sThe view wraps the button in a ListenableBuilder listening to the command, shows a progress indicator while command.running is true, and wires onPressed to execute(), which returns early if the command is already running.
solid answer
~30 sIn the guide, a view model exposes each action as a `Command` object, such as `saveDraft`, instead of a plain method. A `Command` extends `ChangeNotifier`, so the view can pass it straight to `ListenableBuilder(listenable: viewModel.saveDraft, ...)`. Inside the builder, `saveDraft.running` decides between a `CircularProgressIndicator` and the button, and the button's `onPressed` calls `saveDraft.execute(draft)`. Double taps are handled twice over: `execute` returns immediately while `running` is true, and the view can also pass `onPressed: null` while running so the button looks disabled. Because each command has its own `running`, a 'save draft' spinner does not flash when a separate 'load' command runs.
code
dart · 31 linesimport 'package:flutter/material.dart';
import 'command.dart'; // the guide's Command0 / Command1
import 'editor_view_model.dart';
class SaveDraftButton extends StatelessWidget {
const SaveDraftButton({super.key, required this.viewModel});
final EditorViewModel viewModel;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel.saveDraft,
builder: (context, _) {
final running = viewModel.saveDraft.running;
return FilledButton(
onPressed: running
? null // disabled while saving
: () => viewModel.saveDraft.execute(viewModel.draft),
child: running
? const SizedBox.square(
dimension: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Save draft'),
);
},
);
}
}go deeper
Show the wiring: ListenableBuilder on the command, running for the spinner, execute on the button.
Explain why per-action state beats shared flags and how Command0 and Command1 differ.
Point out rebuild scope and lifecycle: commands created once in the view model, listened to separately from the data.
Decide whether the team adopts the guide's Command class, a package, or its state library's equivalent, and keep one convention.
## The problem a command solves In Flutter's architecture guide, a **view model** holds UI state and the actions a view can trigger. Once a screen has two actions, such as loading a blog post and saving a draft, a hand-written view model needs a separate `runningLoad`, `errorLoad`, `runningSave` and `errorSave` for each, plus guard code against running an action twice. The guide extracts that repeated code into a reusable **Command** class: one object per action that owns the action's `running`, `error` and `completed` state. ## What the view sees The guide's `Command` extends **`ChangeNotifier`**. That makes it a `Listenable`, so a view can listen to one action on its own: - `running` is `true` from the moment `execute` starts until the action finishes; - `error` is `true` when the last run finished with a failure; - `completed` is `true` when the last run finished successfully. The view model exposes the command as a public field, for example `late final Command1<void, Draft> saveDraft;`, and keeps the real work in a private method. ## Wiring the 'save draft' button In a blogging app's editor screen: 1. Wrap the button in `ListenableBuilder(listenable: viewModel.saveDraft, builder: ...)`. 2. In the builder, if `viewModel.saveDraft.running` is true, show a `CircularProgressIndicator` or a disabled button. 3. Otherwise show a `FilledButton` whose `onPressed` calls `viewModel.saveDraft.execute(currentDraft)`. The rest of the screen can listen to the view model itself with a second `ListenableBuilder`, so a save does not rebuild the editor text, and loading the post does not flash the save spinner. ## Why double taps are harmless The guide's `execute` begins with a guard: if the command is already running it **returns without doing anything**. A user who taps 'Save draft' three times quickly starts one save, not three. Disabling the button while `running` is still worth doing, because it tells the user why nothing happens. | Without a command | With a command | |---|---| | One `running` flag per action written by hand | `running` lives on each command | | Guard code copied into every method | `execute` refuses to start while running | | View listens to the whole view model | View can listen to just `saveDraft` | | Shared flags make spinners flash for the wrong action | Each action's state is separate | ## Commands with and without arguments The guide ships two variants: **`Command0`** for actions without arguments, whose `execute()` takes nothing, and **`Command1`** for actions with one argument, whose `execute(argument)` passes it through. 'Load post' is naturally a `Command0`; 'save draft' is a `Command1` that receives the draft. A view model can also start a command itself, for example `load = Command0(_load)..execute();` in its constructor so the screen loads on creation. ## Things juniors get wrong - Listening to the view model instead of the command, so the spinner never updates when only the command notifies. - Calling the private `_saveDraft` method from the view, which skips the running state entirely. - Building a new `Command` inside `build()`, which loses its state on every rebuild.
- Why listen to the command rather than the whole view model?A command is its own `ChangeNotifier`. Listening to `viewModel.saveDraft` rebuilds only the button when the save starts or ends, and the spinner reflects that one action instead of any change anywhere in the view model.
- What does Command1 add over Command0?An argument. `Command0.execute()` takes none; `Command1<T, A>.execute(A argument)` passes one value, such as the draft, to the wrapped action. The result type `T` is the same idea in both.
saying these in an interview costs you the question
- Each tap on the button starts another save while one is running.
- The view should call the private save method directly for speed.
- A command cannot be listened to, so the view must poll it.
- One shared running flag on the view model is enough for every action.
- Commands should be created inside build so they are always fresh.