跳转至正文

层间通信

如何通过依赖注入实现 MVVM 各层之间的通信。

除为架构各组件定义清晰职责外,还需考虑组件如何通信—— 既包括约束通信的规则,也包括通信的技术实现。应用架构应回答以下问题:

  • 哪些组件可以与哪些其他组件(含同类型组件)通信?

  • 这些组件向彼此暴露什么作为输出?

  • 任意一层如何与另一层「接线」?

A diagram showing the components of app architecture.

以该图为指南,协作规则如下:

ComponentRules of engagement
View
  1. A view is only aware of exactly one view model, and is never aware of any other layer or component. When created, Flutter passes the view model to the view as an argument, exposing the view model's data and command callbacks to the view.
ViewModel
  1. A ViewModel belongs to exactly one view, which can see its data, but the model never needs to know that a view exists.
  2. A view model is aware of one or more repositories, which are passed into the view model's constructor.
Repository
  1. A repository can be aware of many services, which are passed as arguments into the repository constructor.
  2. A repository can be used by many view models, but it never needs to be aware of them.
Service
  1. A service can be used by many repositories, but it never needs to be aware of a repository (or any other object).
组件协作规则
View
  1. View 仅感知恰好一个 view model,从不感知任何其他层或组件。创建时 Flutter 将 view model 作为参数传给 view,向 view 暴露 view model 的数据与 command 回调。
ViewModel
  1. ViewModel 属于恰好一个 view,view 可见其数据,但 model 无需知道 view 存在。
  2. View model 感知一个或多个通过构造函数传入的仓库。
Repository
  1. 仓库可感知多个通过构造函数参数传入的 service。
  2. 仓库可被多个 view model 使用,但无需感知它们。
Service
  1. Service 可被多个仓库使用,但无需感知仓库(或任何其他对象)。

依赖注入

#

本指南已说明各组件如何通过输入输出通信。层间通信一律通过将组件传入消费方构造函数实现,例如将 Service 传入 Repository

dart
class MyRepository {
  MyRepository({required MyService myService})
          : _myService = myService;

  late final MyService _myService;
}

然而还缺一环:对象创建。应用中 MyService 实例在何处创建以便传入 MyRepository?答案涉及 依赖注入 模式。

在 Compass 中,依赖注入 通过 package:provider 实现。基于构建 Flutter 应用的经验,Google 团队推荐使用 package:provider 实现依赖注入。

Service 与仓库作为 Provider 对象暴露于 Flutter 应用 widget 树顶层。

dependencies.dart
dart
runApp(
  MultiProvider(
    providers: [
      Provider(create: (context) => AuthApiClient()),
      Provider(create: (context) => ApiClient()),
      Provider(create: (context) => SharedPreferencesService()),
      ChangeNotifierProvider(
        create: (context) => AuthRepositoryRemote(
          authApiClient: context.read(),
          apiClient: context.read(),
          sharedPreferencesService: context.read(),
        ) as AuthRepository,
      ),
      Provider(create: (context) =>
        DestinationRepositoryRemote(
          apiClient: context.read(),
        ) as DestinationRepository,
      ),
      Provider(create: (context) =>
        ContinentRepositoryRemote(
          apiClient: context.read(),
        ) as ContinentRepository,
      ),
      // In the Compass app, additional service and repository providers live here.
    ],
    child: const MainApp(),
  ),
);

Service 仅为了立即通过 providerBuildContext.read 注入仓库而暴露,如上一片段所示。随后暴露仓库以便按需注入 view model。

在 widget 树稍低处,对应全屏的 view model 在 package:go_router 配置中创建,再次用 provider 注入所需仓库。

router.dart
dart
// This code was modified for demo purposes.
GoRouter router(
  AuthRepository authRepository,
) =>
    GoRouter(
      initialLocation: Routes.home,
      debugLogDiagnostics: true,
      redirect: _redirect,
      refreshListenable: authRepository,
      routes: [
        GoRoute(
          path: Routes.login,
          builder: (context, state) {
            return LoginScreen(
              viewModel: LoginViewModel(
                authRepository: context.read(),
              ),
            );
          },
        ),
        GoRoute(
          path: Routes.home,
          builder: (context, state) {
            final viewModel = HomeViewModel(
              bookingRepository: context.read(),
            );
            return HomeScreen(viewModel: viewModel);
          },
          routes: [
            // ...
          ],
        ),
      ],
    );

在 view model 或仓库内部,注入的组件应为私有。例如 HomeViewModel 如下:

home_viewmodel.dart
dart
class HomeViewModel extends ChangeNotifier {
  HomeViewModel({
    required BookingRepository bookingRepository,
    required UserRepository userRepository,
  })  : _bookingRepository = bookingRepository,
        _userRepository = userRepository;

  final BookingRepository _bookingRepository;
  final UserRepository _userRepository;

  // ...
}

私有成员防止能访问 view model 的 view 直接调用仓库方法。

Compass 应用代码 walkthrough 到此结束。本页仅涵盖架构相关代码,并非全貌;大部分工具代码、widget 代码与 UI 样式未涉及。请在 Compass 应用仓库 浏览完整示例。

反馈

#

网站本节内容仍在完善中, 欢迎提供反馈