命令模式
模型-檢視-ViewModel (MVVM) 是一種設計模式,它將應用程式的某個功能分解為三個部分:模型、ViewModel 和檢視。檢視和 ViewModel 構成了應用程式的 UI 層。儲存庫和服務代表了應用程式的資料層,或 MVVM 的模型層。
命令是一個類,它封裝了一個方法,並有助於處理該方法的不同狀態,例如正在執行、完成和錯誤。
ViewModel 可以使用命令來處理互動和執行操作。同樣,它們也可以用於顯示不同的 UI 狀態,例如在操作執行時顯示載入指示器,或在操作失敗時顯示錯誤對話方塊。
隨著應用程式的增長和功能的擴大,ViewModel 可能會變得非常複雜。命令可以幫助簡化 ViewModel 和重用程式碼。
在本指南中,您將學習如何使用命令模式來改進您的 ViewModel。
實現 ViewModel 時的挑戰
#Flutter 中的 ViewModel 類通常透過擴充套件 ChangeNotifier 類來實現。這使得 ViewModel 在資料更新時可以呼叫 notifyListeners() 來重新整理檢視。
class HomeViewModel extends ChangeNotifier {
// ···
}ViewModel 包含 UI 狀態的表示,包括正在顯示的資料。例如,這個 HomeViewModel 向檢視公開了 User 例項。
class HomeViewModel extends ChangeNotifier {
User? get user => // ...
// ···
}ViewModel 還包含通常由檢視觸發的操作;例如,負責載入 user 的 load 操作。
class HomeViewModel extends ChangeNotifier {
User? get user => // ...
// ···
void load() {
// load user
}
// ···
}ViewModel 中的 UI 狀態
#ViewModel 除了資料之外,還包含 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
}當 ViewModel 包含多個操作時,管理操作的狀態可能會變得複雜。例如,向 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 狀態的同樣問題。
從 ViewModel 觸發 UI 操作
#ViewModel 類在執行 UI 操作以及 ViewModel 狀態發生變化時可能會遇到問題。
例如,您可能希望在發生錯誤時顯示一個 SnackBar,或者在操作完成時導航到另一個螢幕。要實現這一點,請監聽 ViewModel 中的變化,並根據狀態執行操作。
在檢視中
@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
}
}命令模式
#您可能會發現自己一遍又一遍地重複上述程式碼,為每個 ViewModel 中的每個操作實現不同的執行狀態。此時,將此程式碼提取到一個可重用的模式中是有意義的:命令。
命令是一個封裝了 ViewModel 操作的類,並公開了操作可以具有的不同狀態。
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
}
}在 ViewModel 中,您不是直接用方法定義一個操作,而是建立一個命令物件
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() 方法也可以在 ViewModel 內部呼叫。下面的程式碼行在建立 ViewModel 時執行 load 命令。
HomeViewModel() {
load = Command(_load)..execute();
}execute() 方法將 running 狀態設定為 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 中,不要將 ViewModel 傳遞給 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 狀態,然後再顯示 ViewModel 資料。
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, _) {
// ···
},
),
),您可以在單個 ViewModel 中定義多個命令類,從而簡化其實現並最小化重複程式碼量。
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 設計模式時 ViewModel 的實現。
下面,您可以找到 Flutter 架構指南中 Compass App 示例 中實現的完整 Command 類。它還使用 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));
}
}