Skip to main content

命令模式

模型-视图-视图模型 (MVVM) 是一种设计模式,它将应用程序的一个特性分成三个部分:模型、视图模型和视图。视图和视图模型构成了应用程序的 UI 层。存储库和服务表示应用程序的数据层,或 MVVM 的模型层。

命令是一个包装方法的类,它有助于处理该方法的不同状态,例如运行、完成和错误。

视图模型 可以使用命令来处理交互和运行操作。同样,它们可以用来显示不同的 UI 状态,例如操作运行时的加载指示器,或操作失败时的错误对话框。

随着应用程序的增长和功能的扩大,视图模型可能会变得非常复杂。命令可以帮助简化视图模型并重用代码。

在本指南中,您将学习如何使用命令模式来改进您的视图模型。

实现视图模型时的挑战

#

Flutter 中的视图模型类通常通过扩展 ChangeNotifier 类来实现。这允许视图模型在数据更新时调用 notifyListeners() 来刷新视图。

dart
class HomeViewModel extends ChangeNotifier {
  // ···
}

视图模型包含 UI 状态的表示,包括正在显示的数据。例如,此 HomeViewModelUser 实例公开给视图。

dart
class HomeViewModel extends ChangeNotifier {

  User? get user => // ...
  // ···
}

视图模型还包含通常由视图触发的操作;例如,负责加载 userload 操作。

dart
class HomeViewModel extends ChangeNotifier {

  User? get user => // ...
  // ···
  void load() {
    // load user
  }
  // ···
}

视图模型中的 UI 状态

#

除了数据之外,视图模型还包含 UI 状态,例如视图是否正在运行或遇到错误。这允许应用程序告诉用户操作是否已成功完成。

dart
class HomeViewModel extends ChangeNotifier {

  User? get user => // ...

  bool get running => // ...

  Exception? get error => // ...

  void load() {
    // load user
  }
  // ···
}

您可以使用运行状态在视图中显示进度指示器:

dart
ListenableBuilder(
  listenable: widget.viewModel,
  builder: (context, _) {
    if (widget.viewModel.running) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }
    // ···
  },
)

或使用运行状态来避免多次执行操作:

dart
void load() {
  if (running) {
    return;
  }
  // load user
}

如果视图模型包含多个操作,则管理操作的状态可能会变得复杂。例如,向 HomeViewModel 添加 edit() 操作可能会导致以下结果:

dart
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,或者在操作完成时导航到不同的屏幕。为此,请侦听视图模型中的更改,并根据状态执行操作。

在视图中:

dart
@override
void initState() {
  super.initState();
  widget.viewModel.addListener(_onViewModelChanged);
}

@override
void dispose() {
  widget.viewModel.removeListener(_onViewModelChanged);
  super.dispose();
}
dart
void _onViewModelChanged() {
  if (widget.viewModel.error != null) {
    // 显示 Snackbar
  }
}

每次执行此操作时,都需要清除错误状态,否则每次调用 notifyListeners() 时都会执行此操作。

dart
void _onViewModelChanged() {
  if (widget.viewModel.error != null) {
    widget.viewModel.clearError();
    // 显示 Snackbar
  }
}

命令模式

#

您可能会发现自己一遍遍地重复上述代码,为每个视图模型中的每个操作实现不同的运行状态。此时,将此代码提取到可重用的模式中是有意义的:一个命令。

命令是一个封装视图模型操作并在其状态发生变化时执行的类。

dart
class Command extends ChangeNotifier {
  Command(this._action);

  bool get running => // ...

  Exception? get error => // ...

  bool get completed => // ...

  void Function() _action;

  void execute() {
    // 运行 _action
  }

  void clear() {
    // 清除状态
  }
}

在视图模型中,您不是直接使用方法定义操作,而是创建命令对象:

dart
class HomeViewModel extends ChangeNotifier {
  HomeViewModel() {
    load = Command(_load)..execute();
  }

  User? get user => // ...

  late final Command load;

  void _load() {
    // load user
  }
}

以前的方法 load() 变成 _load(),取而代之的是将命令 load 公开给视图。以前的状态 runningerror 可以删除,因为它们现在是命令的一部分。

执行命令

#

现在您调用 viewModel.load.execute() 来运行加载操作,而不是调用 viewModel.load()

execute() 方法也可以从视图模型内部调用。以下代码行在创建视图模型时运行 load 命令。

dart
HomeViewModel() {
  load = Command(_load)..execute();
}

execute() 方法将运行状态设置为 true 并重置 errorcompleted 状态。当操作完成时,running 状态更改为 falsecompleted 状态更改为 true

如果 running 状态为 true,则命令无法再次开始执行。这可以防止用户通过快速按下按钮多次触发命令。

命令的 execute() 方法会自动捕获任何抛出的 Exception 并将其公开在 error 状态中。

以下代码显示了一个简化的 Command 类示例,为了演示目的进行了简化。您可以在本页末尾看到完整的实现。

dart
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,而是传递命令:

dart
ListenableBuilder(
  listenable: widget.viewModel.load,
  builder: (context, child) {
    if (widget.viewModel.load.running) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }
  // ···
)

并侦听命令状态的变化以运行 UI 操作:

dart
@override
void initState() {
  super.initState();
  widget.viewModel.addListener(_onViewModelChanged);
}

@override
void dispose() {
  widget.viewModel.removeListener(_onViewModelChanged);
  super.dispose();
}
dart
void _onViewModelChanged() {
  if (widget.viewModel.load.error != null) {
    widget.viewModel.load.clear();
    // 显示 Snackbar
  }
}

命令和 ViewModel 的组合

#

您可以堆叠多个 ListenableBuilder 小部件来侦听 runningerror 状态,然后显示视图模型数据。

dart
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, _) {
      // ···
    },
  ),
),

您可以在单个视图模型中定义多个命令类,从而简化其实现并最大限度地减少重复代码的数量。

dart
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
  }
}

扩展命令模式

#

命令模式可以多种方式扩展。例如,支持不同数量的参数。

dart
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(用于带有一个参数的操作)。

dart
// 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));
  }
}