命令模式
通过实现 Command 类简化 view model 逻辑。
Model-View-ViewModel (MVVM) 是一种设计模式,将应用的一个功能拆为 model、view model 与 view 三部分。 View 与 view model 构成应用的 UI 层;repository 与 service 代表数据层,即 MVVM 的 model 层。
Command 是包装方法并帮助处理方法各状态(如 running、complete、error)的类。
View model 可用 command 处理交互与执行操作,也可用于展示不同 UI 状态,例如操作进行中显示加载指示器,失败时显示错误对话框。
随应用与功能增长,view model 可能变得非常复杂;command 有助于简化 view model 并复用代码。
本指南介绍如何使用命令模式改进 view model。
实现 view model 时的挑战
#
Flutter 中的 view model 通常通过继承 ChangeNotifier
实现,以便在数据更新时调用 notifyListeners() 刷新 view。
class HomeViewModel extends ChangeNotifier {
// ···
}
View model 包含 UI 状态表示,包括展示的数据。例如该 HomeViewModel 向 view 暴露 User 实例。
class HomeViewModel extends ChangeNotifier {
User? get user => // ...
// ···
}
View model 还包含通常由 view 触发的操作,例如负责加载 user 的 load 操作。
class HomeViewModel extends ChangeNotifier {
User? get user => // ...
// ···
void load() {
// load user
}
// ···
}
View model 中的 UI 状态
#除数据外,view model 还包含 UI 状态,例如是否正在运行或是否出错,以便告知用户操作是否成功完成。
class HomeViewModel extends ChangeNotifier {
User? get user => // ...
bool get running => // ...
Exception? get error => // ...
void load() {
// load user
}
// ···
}
可用 running 状态在 view 中显示进度指示器:
ListenableBuilder(
listenable: widget.viewModel,
builder: (context, _) {
if (widget.viewModel.running) {
return const Center(child: CircularProgressIndicator());
}
// ···
},
)
或用 running 状态避免重复执行操作:
void load() {
if (running) {
return;
}
// load user
}
若 view model 包含多个操作,管理操作状态会变复杂。例如向 HomeViewModel 添加 edit() 可能导致:
class HomeViewModel extends ChangeNotifier {
User? get user => // ...
bool get runningLoad => // ...
Exception? get errorLoad => // ...
bool get runningEdit => // ...
Exception? get errorEdit => // ...
void load() {
// load user
}
void edit(String name) {
// edit user
}
}
在 load() 与 edit() 间共享 running 状态未必可行,因两种操作可能需要不同 UI 组件;error 状态也有同样问题。
从 view model 触发 UI 操作
#执行 UI 操作且 view model 状态变化时,view model 可能遇到问题。
例如出错时显示 SnackBar,操作完成时导航到其他屏幕。实现方式是监听 view model 变化并按状态执行操作。
在 view 中:
@override
void initState() {
super.initState();
widget.viewModel.addListener(_onViewModelChanged);
}
@override
void dispose() {
widget.viewModel.removeListener(_onViewModelChanged);
super.dispose();
}
void _onViewModelChanged() {
if (widget.viewModel.error != null) {
// Show Snackbar
}
}
每次执行该操作后须清除 error 状态,否则每次 notifyListeners() 都会重复触发。
void _onViewModelChanged() {
if (widget.viewModel.error != null) {
widget.viewModel.clearError();
// Show Snackbar
}
}
命令模式
#你可能反复编写上述代码,为每个 view model 的每个操作实现不同 running 状态。此时将代码提取为可复用的 command 模式是合理的。
Command 封装 view model 操作并暴露操作可能处于的各种状态。
class Command extends ChangeNotifier {
Command(this._action);
bool get running => // ...
Exception? get error => // ...
bool get completed => // ...
void Function() _action;
void execute() {
// run _action
}
void clear() {
// clear state
}
}
在 view model 中,不直接用方法定义操作,而是创建 command 对象:
class HomeViewModel extends ChangeNotifier {
HomeViewModel() {
load = Command(_load)..execute();
}
User? get user => // ...
late final Command load;
void _load() {
// load user
}
}
原 load() 变为 _load(),向 View 暴露 command load;原 running 与 error 可移除,已归入 command。
执行 command
#不再调用 viewModel.load(),而调用 viewModel.load.execute()。
execute() 也可在 view model 内部调用;以下代码在创建 view model 时运行 load command。
HomeViewModel() {
load = Command(_load)..execute();
}
execute() 将 running 设为 true 并重置 error 与 completed;操作结束时 running 为 false,completed 为 true。
running 为 true 时 command 无法再次执行,防止用户快速连按重复触发。
command 的 execute() 自动捕获抛出的 Exception 并暴露在 error 状态。
以下为简化的 Command 示例,完整实现见文末。
class Command extends ChangeNotifier {
Command(this._action);
bool _running = false;
bool get running => _running;
Exception? _error;
Exception? get error => _error;
bool _completed = false;
bool get completed => _completed;
final Future<void> Function() _action;
Future<void> execute() async {
if (_running) {
return;
}
_running = true;
_completed = false;
_error = null;
notifyListeners();
try {
await _action();
_completed = true;
} on Exception catch (error) {
_error = error;
} finally {
_running = false;
notifyListeners();
}
}
void clear() {
_running = false;
_error = null;
_completed = false;
}
}
监听 command 状态
#Command 继承 ChangeNotifier,view 可监听其状态。
在 ListenableBuilder 中,将 command 传给 listenable,而非整个 view model:
ListenableBuilder(
listenable: widget.viewModel.load,
builder: (context, child) {
if (widget.viewModel.load.running) {
return const Center(child: CircularProgressIndicator());
}
// ···
)
监听 command 状态变化以执行 UI 操作:
@override
void initState() {
super.initState();
widget.viewModel.addListener(_onViewModelChanged);
}
@override
void dispose() {
widget.viewModel.removeListener(_onViewModelChanged);
super.dispose();
}
void _onViewModelChanged() {
if (widget.viewModel.load.error != null) {
widget.viewModel.load.clear();
// Show Snackbar
}
}
组合 command 与 ViewModel
#
可堆叠多个 ListenableBuilder,在展示 view model 数据前监听 running 与 error 状态。
body: ListenableBuilder(
listenable: widget.viewModel.load,
builder: (context, child) {
if (widget.viewModel.load.running) {
return const Center(child: CircularProgressIndicator());
}
if (widget.viewModel.load.error != null) {
return Center(
child: Text('Error: ${widget.viewModel.load.error}'),
);
}
return child!;
},
child: ListenableBuilder(
listenable: widget.viewModel,
builder: (context, _) {
// ···
},
),
),
可在单个 view model 中定义多个 command 类,简化实现并减少重复代码。
class HomeViewModel2 extends ChangeNotifier {
HomeViewModel2() {
load = Command(_load)..execute();
delete = Command(_delete);
}
User? get user => // ...
late final Command load;
late final Command delete;
Future<void> _load() async {
// load user
}
Future<void> _delete() async {
// delete user
}
}
扩展命令模式
#命令模式可通过多种方式扩展,例如支持不同数量的参数。
class HomeViewModel extends ChangeNotifier {
HomeViewModel() {
load = Command0(_load)..execute();
edit = Command1<String>(_edit);
}
User? get user => // ...
// Command0 accepts 0 arguments
late final Command0 load;
// Command1 accepts 1 argument
late final Command1<String> edit;
Future<void> _load() async {
// load user
}
Future<void> _edit(String name) async {
// edit user
}
}
总结
#本指南介绍了在使用 MVVM 时如何用命令设计模式改进 view model 实现。
下文为 Flutter 架构指南 Compass 应用示例 中的完整
Command 类,并使用 Result 类 判断操作成功或失败。
该实现包含 Command0(无参)与 Command1(单参)两种 command。
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'result.dart';
/// Defines a command action that returns a [Result] of type [T].
/// Used by [Command0] for actions without arguments.
typedef CommandAction0<T> = Future<Result<T>> Function();
/// Defines a command action that returns a [Result] of type [T].
/// Takes an argument of type [A].
/// Used by [Command1] for actions with one argument.
typedef CommandAction1<T, A> = Future<Result<T>> Function(A);
/// Facilitates interaction with a view model.
///
/// Encapsulates an action,
/// exposes its running and error states,
/// and ensures that it can't be launched again until it finishes.
///
/// Use [Command0] for actions without arguments.
/// Use [Command1] for actions with one argument.
///
/// Actions must return a [Result] of type [T].
///
/// Consume the action result by listening to changes,
/// then call to [clearResult] when the state is consumed.
abstract class Command<T> extends ChangeNotifier {
bool _running = false;
/// Whether the action is running.
bool get running => _running;
Result<T>? _result;
/// Whether the action completed with an error.
bool get error => _result is Error;
/// Whether the action completed successfully.
bool get completed => _result is Ok;
/// The result of the most recent action.
///
/// Returns `null` if the action is running or completed with an error.
Result<T>? get result => _result;
/// Clears the most recent action's result.
void clearResult() {
_result = null;
notifyListeners();
}
/// Execute the provided [action], notifying listeners and
/// setting the running and result states as necessary.
Future<void> _execute(CommandAction0<T> action) async {
// Ensure the action can't launch multiple times.
// e.g. avoid multiple taps on button
if (_running) return;
// Notify listeners.
// e.g. button shows loading state
_running = true;
_result = null;
notifyListeners();
try {
_result = await action();
} finally {
_running = false;
notifyListeners();
}
}
}
/// A [Command] that accepts no arguments.
final class Command0<T> extends Command<T> {
/// Creates a [Command0] with the provided [CommandAction0].
Command0(this._action);
final CommandAction0<T> _action;
/// Executes the action.
Future<void> execute() async {
await _execute(_action);
}
}
/// A [Command] that accepts one argument.
final class Command1<T, A> extends Command<T> {
/// Creates a [Command1] with the provided [CommandAction1].
Command1(this._action);
final CommandAction1<T, A> _action;
/// Executes the action with the specified [argument].
Future<void> execute(A argument) async {
await _execute(() => _action(argument));
}
}
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-22。查看文档源码 或者 为本页面内容提出建议。