跳到主内容

层级间通信

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

在为架构的每个组件定义明确职责的同时,考虑组件之间如何通信也同样重要。这既指规定通信的规则,也指组件通信的技术实现。应用程序的架构应回答以下问题:

  • 哪些组件被允许与哪些其他组件(包括同类型的组件)进行通信?
  • 这些组件向彼此公开了哪些输出?
  • 给定层级是如何与另一层级“连接(wired up)”的?

A diagram showing the components of app architecture.

以该图表作为指南,交互规则如下:

组件交互规则
View(视图)
  1. 视图仅了解与其对应的唯一一个视图模型(ViewModel),绝不会了解任何其他层级或组件。在创建时,Flutter 会将视图模型作为参数传递给视图,向视图公开视图模型的数据和命令回调。
ViewModel(视图模型)
  1. 视图模型仅属于一个视图,该视图可以看到其数据,但模型无需知道视图的存在。
  2. 视图模型了解一个或多个存储库(Repository),这些存储库通过视图模型的构造函数传入。
Repository(存储库)
  1. 存储库可以了解多个服务(Service),这些服务作为参数传入存储库的构造函数中。
  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 应用程序组件树的顶层。

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(),
  ),
);

服务仅为了能立即通过 providerBuildContext.read 方法注入到存储库中而被公开,如前述代码片段所示。随后,存储库被公开以便根据需要注入到视图模型中。

在组件树更低的位置,对应于整个屏幕的视图模型是在 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 应用的代码演练。本页面仅介绍了与架构相关的代码,并未涵盖全部内容。大多数工具代码、组件代码和 UI 样式都被忽略了。请浏览 Compass 应用代码库,以获取遵循这些原则构建的健壮 Flutter 应用程序的完整示例。

反馈

#

随着网站这一部分的不断完善,我们欢迎您提供反馈