跳到主内容

层与层之间的通信

如何实现依赖注入以在 MVVM 层之间进行通信。

除了为架构的每个组件定义明确的职责外,还必须考虑组件如何通信。这既指规定通信的规则,也指组件通信的技术实现。应用程序的架构应该回答以下问题

  • 哪些组件允许与哪些其他组件通信(包括相同类型的组件)?
  • 这些组件将哪些内容作为输出暴露给彼此?
  • 如何将任何给定的层“连接”到另一个层?

A diagram showing the components of app architecture.

以这个图表为指导,参与规则如下

组件参与规则
视图 (View)
  1. 视图只了解一个视图模型,并且不了解任何其他层或组件。创建时,Flutter 将视图模型作为参数传递给视图,将视图模型的数据和命令回调暴露给视图。
视图模型 (ViewModel)
  1. 视图模型属于一个视图,该视图可以看到其数据,但模型不需要知道视图是否存在。
  2. 视图模型了解一个或多个仓库,这些仓库被传递到视图模型的构造函数中。
仓库 (Repository)
  1. 仓库可以了解许多服务,这些服务作为参数传递到仓库构造函数中。
  2. 许多视图模型可以使用仓库,但仓库不需要了解它们。
服务 (Service)
  1. 许多仓库可以使用服务,但服务不需要了解仓库(或任何其他对象)。

依赖注入

#

本指南展示了这些不同组件如何通过使用输入和输出来相互通信。在每种情况下,两个层之间的通信都通过将组件传递到构造函数方法(消耗其数据的组件)来促进,例如将 Service 传递到 Repository

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

  late final MyService _myService;
}

然而,缺少的一件事是对象创建。在应用程序中,MyService 实例在哪里创建,以便可以将其传递到 MyRepository?这个问题涉及一种称为 依赖注入 的模式。

在 Compass 应用程序中,依赖注入 使用 package:provider 处理。根据他们在构建 Flutter 应用程序方面的经验,Google 团队建议使用 package:provider 来实现依赖注入。

服务和仓库作为 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(),
  ),
);

服务仅被暴露,以便它们可以立即通过 provider 中的 BuildContext.read 方法注入到仓库中,如前面的代码片段所示。然后暴露仓库,以便根据需要将其注入到视图模型中。

在 widget 树的稍低层级,与完整屏幕对应的视图模型在 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: [
            // ...
          ],
        ),
      ],
    );

在视图模型或仓库内部,注入的组件应该是私有的。例如,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;

  // ...
}

私有方法可以防止视图(它可以访问视图模型)直接调用仓库上的方法。

这完成了 Compass 应用程序的代码演练。此页面仅演练了与架构相关的代码,但并没有讲述完整的故事。大多数实用代码、widget 代码和 UI 样式都被忽略了。浏览 Compass 应用程序仓库 以获取遵循这些原则构建的强大 Flutter 应用程序的完整示例。

反馈

#

由于本网站的这一部分正在不断发展,我们 欢迎您的反馈