跳到主內容

層與層之間的通訊

如何實現依賴注入以在 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 應用程式的完整示例。

反饋

#

由於本網站的這一部分正在不斷發展,我們 歡迎您的反饋