命令模式
Model-View-ViewModel (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) {
// Show Snackbar
}
}
每次执行此操作时,都需要清除错误状态,否则每次调用 notifyListeners()
时都会发生此操作。
void _onViewModelChanged() {
if (widget.viewModel.error != null) {
widget.viewModel.clearError();
// Show Snackbar
}
}
命令模式
#您可能会发现自己一遍又一遍地重复上述代码,为每个视图模型中的每个操作实现不同的运行状态。此时,将此代码提取到一个可重用模式中是很有意义的:一个命令。
命令是一个封装视图模型操作并公开操作可能具有的不同状态的类。
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
}
}
在视图模型中,您不是直接用方法定义操作,而是创建一个命令对象
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()
来运行加载操作,而是调用 viewModel.load.execute()
。
execute()
方法也可以从视图模型内部调用。以下代码行在视图模型创建时运行 load
命令。
HomeViewModel() {
load = Command(_load)..execute();
}
execute()
方法将运行状态设置为 true
并重置 error
和 completed
状态。当操作完成时,running
状态变为 false
,completed
状态变为 true
。
如果 running
状态为 true
,则命令不能再次开始执行。这可以防止用户通过快速按下按钮多次触发命令。
命令的 execute()
方法自动捕获任何抛出的 Exceptions
并在 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();
// Show 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 accepts 0 arguments
late final Command0 load;
// Command1 accepts 1 argument
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';
/// 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));
}
}