命令模式
模型-视图-视图模型 (MVVM) 是一种设计模式,它将应用程序的一个特性分成三个部分:模型、视图模型和视图。视图和视图模型构成了应用程序的 UI 层。存储库和服务表示应用程序的数据层,或 MVVM 的模型层。
命令是一个包装方法的类,它有助于处理该方法的不同状态,例如运行、完成和错误。
视图模型 可以使用命令来处理交互和运行操作。同样,它们可以用来显示不同的 UI 状态,例如操作运行时的加载指示器,或操作失败时的错误对话框。
随着应用程序的增长和功能的扩大,视图模型可能会变得非常复杂。命令可以帮助简化视图模型并重用代码。
在本指南中,您将学习如何使用命令模式来改进您的视图模型。
实现视图模型时的挑战
#Flutter 中的视图模型类通常通过扩展 ChangeNotifier
类来实现。这允许视图模型在数据更新时调用 notifyListeners()
来刷新视图。
class HomeViewModel extends ChangeNotifier {
// ···
}
视图模型包含 UI 状态的表示,包括正在显示的数据。例如,此 HomeViewModel
将 User
实例公开给视图。
class HomeViewModel extends ChangeNotifier {
User? get user => // ...
// ···
}
视图模型还包含通常由视图触发的操作;例如,负责加载 user
的 load
操作。
class HomeViewModel extends ChangeNotifier {
User? get user => // ...
// ···
void load() {
// load user
}
// ···
}
视图模型中的 UI 状态
#除了数据之外,视图模型还包含 UI 状态,例如视图是否正在运行或遇到错误。这允许应用程序告诉用户操作是否已成功完成。
class HomeViewModel extends ChangeNotifier {
User? get user => // ...
bool get running => // ...
Exception? get error => // ...
void load() {
// load user
}
// ···
}
您可以使用运行状态在视图中显示进度指示器:
ListenableBuilder(
listenable: widget.viewModel,
builder: (context, _) {
if (widget.viewModel.running) {
return const Center(
child: CircularProgressIndicator(),
);
}
// ···
},
)
或使用运行状态来避免多次执行操作:
void load() {
if (running) {
return;
}
// load user
}
如果视图模型包含多个操作,则管理操作的状态可能会变得复杂。例如,向 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()
操作之间共享运行状态可能并不总是有效,因为当 load()
操作运行时,您可能希望显示与 edit()
操作运行时不同的 UI 组件,并且您在 error
状态下也会遇到同样的问题。
从视图模型触发 UI 操作
#当执行 UI 操作和视图模型的状态发生变化时,视图模型类可能会遇到问题。
例如,您可能希望在发生错误时显示 SnackBar
,或者在操作完成时导航到不同的屏幕。为此,请侦听视图模型中的更改,并根据状态执行操作。
在视图中:
@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) {
// 显示 Snackbar
}
}
每次执行此操作时,都需要清除错误状态,否则每次调用 notifyListeners()
时都会执行此操作。
void _onViewModelChanged() {
if (widget.viewModel.error != null) {
widget.viewModel.clearError();
// 显示 Snackbar
}
}
命令模式
#您可能会发现自己一遍遍地重复上述代码,为每个视图模型中的每个操作实现不同的运行状态。此时,将此代码提取到可重用的模式中是有意义的:一个命令。
命令是一个封装视图模型操作并在其状态发生变化时执行的类。
class Command extends ChangeNotifier {
Command(this._action);
bool get running => // ...
Exception? get error => // ...
bool get completed => // ...
void Function() _action;
void execute() {
// 运行 _action
}
void clear() {
// 清除状态
}
}
在视图模型中,您不是直接使用方法定义操作,而是创建命令对象:
class HomeViewModel extends ChangeNotifier {
HomeViewModel() {
load = Command(_load)..execute();
}
User? get user => // ...
late final Command load;
void _load() {
// load user
}
}
以前的方法 load()
变成 _load()
,取而代之的是将命令 load
公开给视图。以前的状态 running
和 error
可以删除,因为它们现在是命令的一部分。
执行命令
#现在您调用 viewModel.load.execute()
来运行加载操作,而不是调用 viewModel.load()
。
execute()
方法也可以从视图模型内部调用。以下代码行在创建视图模型时运行 load
命令。
HomeViewModel() {
load = Command(_load)..execute();
}
execute()
方法将运行状态设置为 true
并重置 error
和 completed
状态。当操作完成时,running
状态更改为 false
,completed
状态更改为 true
。
如果 running
状态为 true
,则命令无法再次开始执行。这可以防止用户通过快速按下按钮多次触发命令。
命令的 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
类扩展自 ChangeNotifier
,允许视图侦听其状态。
在 ListenableBuilder
中,不是将视图模型传递给 ListenableBuilder.listenable
,而是传递命令:
ListenableBuilder(
listenable: widget.viewModel.load,
builder: (context, child) {
if (widget.viewModel.load.running) {
return const Center(
child: CircularProgressIndicator(),
);
}
// ···
)
并侦听命令状态的变化以运行 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();
// 显示 Snackbar
}
}
命令和 ViewModel 的组合
#您可以堆叠多个 ListenableBuilder
小部件来侦听 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, _) {
// ···
},
),
),
您可以在单个视图模型中定义多个命令类,从而简化其实现并最大限度地减少重复代码的数量。
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 接受 0 个参数
late final Command0 load;
// Command1 接受 1 个参数
late final Command1 edit;
Future<void> _load() async {
// load user
}
Future<void> _edit(String name) async {
// edit user
}
}
综合运用
#在本指南中,您学习了如何使用命令设计模式来改进使用 MVVM 设计模式时视图模型的实现。
下面,您可以找到完整的 Command
类
正如在 Flutter 架构指南的指南针应用示例中实现的那样。它还使用Result
类来确定操作是成功完成还是出错。
此实现还包括两种类型的命令:Command0
(用于无参数的操作)和 Command1
(用于带有一个参数的操作)。
// 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';
/// 定义一个返回类型为 [T] 的 [Result] 的命令操作。
/// 用于 [Command0](无参数的操作)。
typedef CommandAction0<T> = Future<Result<T>> Function();
/// 定义一个返回类型为 [T] 的 [Result] 的命令操作。
/// 接受类型为 [A] 的参数。
/// 用于 [Command1](带有一个参数的操作)。
typedef CommandAction1<T, A> = Future<Result<T>> Function(A);
/// 促进与视图模型的交互。
///
/// 封装一个操作,
/// 公开其运行和错误状态,
/// 并确保它在完成之前无法再次启动。
///
/// 对于没有参数的操作,使用 [Command0]。
/// 对于带有一个参数的操作,使用 [Command1]。
///
/// 操作必须返回类型为 [T] 的 [Result]。
///
/// 通过侦听更改来使用操作结果,
/// 然后在使用状态后调用 [clearResult]。
abstract class Command<T> extends ChangeNotifier {
bool _running = false;
/// 操作是否正在运行。
bool get running => _running;
Result<T>? _result;
/// 操作是否出错完成。
bool get error => _result is Error;
/// 操作是否成功完成。
bool get completed => _result is Ok;
/// 最近一次操作的结果。
///
/// 如果操作正在运行或出错完成,则返回 `null`。
Result<T>? get result => _result;
/// 清除最近一次操作的结果。
void clearResult() {
_result = null;
notifyListeners();
}
/// 执行提供的 [action],通知侦听器并根据需要设置运行和结果状态。
Future<void> _execute(CommandAction0<T> action) async {
// 确保操作不能多次启动。
// 例如,避免多次点击按钮
if (_running) return;
// 通知侦听器。
// 例如,按钮显示加载状态
_running = true;
_result = null;
notifyListeners();
try {
_result = await action();
} finally {
_running = false;
notifyListeners();
}
}
}
/// 一个不接受参数的 [Command]。
final class Command0<T> extends Command<T> {
/// 使用提供的 [CommandAction0] 创建一个 [Command0]。
Command0(this._action);
final CommandAction0<T> _action;
/// 执行操作。
Future<void> execute() async {
await _execute(() => _action());
}
}
/// 一个接受一个参数的 [Command]。
final class Command1<T, A> extends Command<T> {
/// 使用提供的 [CommandAction1] 创建一个 [Command1]。
Command1(this._action);
final CommandAction1<T, A> _action;
/// 使用指定的 [argument] 执行操作。
Future<void> execute(A argument) async {
await _execute(() => _action(argument));
}
}
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。