除了為架構的每個元件定義清晰的職責外,還需要考慮元件如何通訊。這既包括規定通訊規則,也包括元件如何通訊的技術實現。應用程式的架構應該回答以下問題:

  • 哪些元件可以與其他哪些元件通訊(包括同類型的元件)?
  • 這些元件之間互相暴露哪些輸出?
  • 任何給定的層是如何“連線”到另一層的?

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

反饋

#

隨著本網站該部分的不斷發展,我們歡迎您的反饋