跳至主要內容

層級之間的溝通

除了為架構的每個元件定義清晰的職責之外,考量元件如何溝通也很重要。這指的是規範溝通的規則,以及元件如何溝通的技術實作。應用程式的架構應回答以下問題

  • 哪些元件允許與其他哪些元件溝通(包括相同類型的元件)?
  • 這些元件彼此暴露哪些輸出?
  • 任何給定層級如何「連接」到另一個層級?

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 應用程式的 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 應用程式的完整範例。

意見回饋

#

由於本網站的這個部分正在發展中,我們歡迎您的意見回饋