跳到主內容

層級間通訊

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

反饋

#

隨著網站這一部分的不斷完善,我們歡迎您提供反饋