Model-View-ViewModel (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) {
    // Show Snackbar
  }
}

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

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

命令模式

#

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

命令是一个封装视图模型操作并公开操作可能具有的不同状态的类。

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

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

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() 来运行加载操作,而是调用 viewModel.load.execute()

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

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

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

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

命令的 execute() 方法自动捕获任何抛出的 Exceptions 并在 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();
    // Show 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 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(用于带一个参数的操作)。

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';

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