跳转至正文

命令模式

通过实现 Command 类简化 view model 逻辑。

Model-View-ViewModel (MVVM) 是一种设计模式,将应用的一个功能拆为 model、view model 与 view 三部分。 View 与 view model 构成应用的 UI 层;repository 与 service 代表数据层,即 MVVM 的 model 层。

Command 是包装方法并帮助处理方法各状态(如 running、complete、error)的类。

View model 可用 command 处理交互与执行操作,也可用于展示不同 UI 状态,例如操作进行中显示加载指示器,失败时显示错误对话框。

随应用与功能增长,view model 可能变得非常复杂;command 有助于简化 view model 并复用代码。

本指南介绍如何使用命令模式改进 view model。

实现 view model 时的挑战

#

Flutter 中的 view model 通常通过继承 ChangeNotifier 实现,以便在数据更新时调用 notifyListeners() 刷新 view。

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

View model 包含 UI 状态表示,包括展示的数据。例如该 HomeViewModel 向 view 暴露 User 实例。

dart
class HomeViewModel extends ChangeNotifier {

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

View model 还包含通常由 view 触发的操作,例如负责加载 userload 操作。

dart
class HomeViewModel extends ChangeNotifier {

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

View model 中的 UI 状态

#

除数据外,view model 还包含 UI 状态,例如是否正在运行或是否出错,以便告知用户操作是否成功完成。

dart
class HomeViewModel extends ChangeNotifier {

  User? get user => // ...

  bool get running => // ...

  Exception? get error => // ...

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

可用 running 状态在 view 中显示进度指示器:

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

或用 running 状态避免重复执行操作:

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

若 view model 包含多个操作,管理操作状态会变复杂。例如向 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() 间共享 running 状态未必可行,因两种操作可能需要不同 UI 组件;error 状态也有同样问题。

从 view model 触发 UI 操作

#

执行 UI 操作且 view model 状态变化时,view model 可能遇到问题。

例如出错时显示 SnackBar,操作完成时导航到其他屏幕。实现方式是监听 view model 变化并按状态执行操作。

在 view 中:

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

每次执行该操作后须清除 error 状态,否则每次 notifyListeners() 都会重复触发。

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

命令模式

#

你可能反复编写上述代码,为每个 view model 的每个操作实现不同 running 状态。此时将代码提取为可复用的 command 模式是合理的。

Command 封装 view model 操作并暴露操作可能处于的各种状态。

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

在 view model 中,不直接用方法定义操作,而是创建 command 对象:

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

  User? get user => // ...

  late final Command load;

  void _load() {
    // load user
  }
}

load() 变为 _load(),向 View 暴露 command load;原 runningerror 可移除,已归入 command。

执行 command

#

不再调用 viewModel.load(),而调用 viewModel.load.execute()

execute() 也可在 view model 内部调用;以下代码在创建 view model 时运行 load command。

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

execute() 将 running 设为 true 并重置 errorcompleted;操作结束时 running 为 false,completed 为 true

running 为 true 时 command 无法再次执行,防止用户快速连按重复触发。

command 的 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 状态

#

Command 继承 ChangeNotifier,view 可监听其状态。

ListenableBuilder 中,将 command 传给 listenable,而非整个 view model:

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

监听 command 状态变化以执行 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
  }
}

组合 command 与 ViewModel

#

可堆叠多个 ListenableBuilder,在展示 view model 数据前监听 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, _) {
      // ···
    },
  ),
),

可在单个 view model 中定义多个 command 类,简化实现并减少重复代码。

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<String> edit;

  Future<void> _load() async {
    // load user
  }

  Future<void> _edit(String name) async {
    // edit user
  }
}

总结

#

本指南介绍了在使用 MVVM 时如何用命令设计模式改进 view model 实现。

下文为 Flutter 架构指南 Compass 应用示例 中的完整 Command 类,并使用 Result 判断操作成功或失败。

该实现包含 Command0(无参)与 Command1(单参)两种 command。

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