flutter-apply-architecture-best-practices

flutter-apply-architecture-best-practices

熱門

使用推薦的分層方法(UI、邏輯、資料)來架構 Flutter 應用程式。適用於建立新專案或重構以提升可擴展性。

2525星標
152分支
更新於 2026/6/18
SKILL.md
readonlyread-only
name
flutter-apply-architecture-best-practices
description

使用推薦的分層方法(UI、邏輯、資料)來架構 Flutter 應用程式。適用於建立新專案或重構以提升可擴展性。

架構 Flutter 應用程式

目錄

架構層級

透過將應用程式劃分為不同的層級來強制執行嚴格的關注點分離。切勿將 UI 渲染與業務邏輯或資料擷取混在一起。

UI 層(表現層)

實作 MVVM(Model-View-ViewModel)模式來管理 UI 狀態與邏輯。

  • Views: 撰寫可重複使用、精簡的 widget。將 View 中的邏輯限制在 UI 特定的操作(例如動畫、佈局約束、簡單路由)。從 ViewModel 傳入所有必要的資料。
  • ViewModels: 管理 UI 狀態並處理使用者互動。繼承 ChangeNotifier(或使用 Listenable)來暴露狀態。向 View 暴露不可變的狀態快照。透過建構子將 Repository 注入 ViewModel。

資料層

實作 Repository 模式來隔離資料存取邏輯,並建立單一事實來源。

  • Services: 建立無狀態類別來包裝外部 API(HTTP 客戶端、本地資料庫、平台插件)。回傳原始 API 模型或 Result 包裝器。
  • Repositories: 使用一個或多個 Services。將原始 API 模型轉換為乾淨的領域模型。處理快取、離線同步和重試邏輯。向 ViewModel 暴露領域模型。

邏輯層(領域層 - 可選)

  • Use Cases: 僅在應用程式包含複雜的業務邏輯,導致 ViewModel 過於龐大,或邏輯需要在多個 ViewModel 之間重複使用時,才實作此層級。將此邏輯提取到專門的 Use Case(互動器)類別中,這些類別位於 ViewModel 和 Repository 之間。

專案結構

使用混合方法組織程式碼庫:按功能分組 UI 元件,按類型分組資料/領域元件。

lib/
├── data/
│   ├── models/         # API 模型
│   ├── repositories/   # Repository 實作
│   └── services/       # API 客戶端、本地儲存包裝器
├── domain/
│   ├── models/         # 乾淨的領域模型
│   └── use_cases/      # 可選的業務邏輯類別
└── ui/
    ├── core/           # 共用 widget、主題、排版
    └── features/
        └── [feature_name]/
            ├── view_models/
            └── views/

工作流程:實作新功能

在應用程式中新增功能時,請遵循此順序工作流程。複製核取清單以追蹤進度。

任務進度

  • [ ] 步驟 1:定義領域模型。 使用 freezedbuilt_value 為該功能建立不可變的資料類別。
  • [ ] 步驟 2:實作 Services。 建立或更新 Service 類別以處理外部 API 通訊。
  • [ ] 步驟 3:實作 Repositories。 建立 Repository 以使用 Services 並回傳領域模型。
  • [ ] 步驟 4:應用條件邏輯(領域層)。
    • 如果該功能需要複雜的資料轉換或跨 Repository 邏輯: 建立 Use Case 類別。
    • 如果該功能是簡單的 CRUD 操作: 跳至步驟 5。
  • [ ] 步驟 5:實作 ViewModel。 建立繼承 ChangeNotifier 的 ViewModel。注入所需的 Repositories/Use Cases。暴露不可變的狀態和命令方法。
  • [ ] 步驟 6:實作 View。 建立 UI widget。使用 ListenableBuilderAnimatedBuilder 來監聽 ViewModel 的變更。
  • [ ] 步驟 7:注入依賴。 在依賴注入容器(例如 providerget_it)中註冊新的 Service、Repository 和 ViewModel。
  • [ ] 步驟 8:執行驗證器。 對 ViewModel 和 Repository 執行單元測試。
    • 反饋迴圈: 執行測試 -> 檢視失敗 -> 修正邏輯 -> 重新執行直到通過。

範例

資料層:Service 和 Repository

// 1. Service(原始 API 互動)
class ApiClient {
  Future<UserApiModel> fetchUser(String id) async {
    // HTTP GET 實作...
  }
}

// 2. Repository(單一事實來源,回傳領域模型)
class UserRepository {
  UserRepository({required ApiClient apiClient}) : _apiClient = apiClient;
  
  final ApiClient _apiClient;
  User? _cachedUser;

  Future<User> getUser(String id) async {
    if (_cachedUser != null) return _cachedUser!;
    
    final apiModel = await _apiClient.fetchUser(id);
    _cachedUser = User(id: apiModel.id, name: apiModel.fullName); // 轉換為領域模型
    return _cachedUser!;
  }
}

UI 層:ViewModel 和 View

// 3. ViewModel(狀態管理與表現邏輯)
class ProfileViewModel extends ChangeNotifier {
  ProfileViewModel({required UserRepository userRepository}) 
      : _userRepository = userRepository;

  final UserRepository _userRepository;

  User? _user;
  User? get user => _user;

  bool _isLoading = false;
  bool get isLoading => _isLoading;

  Future<void> loadProfile(String id) async {
    _isLoading = true;
    notifyListeners();

    try {
      _user = await _userRepository.getUser(id);
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

// 4. View(被動 UI 元件)
class ProfileView extends StatelessWidget {
  const ProfileView({super.key, required this.viewModel});

  final ProfileViewModel viewModel;

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: viewModel,
      builder: (context, _) {
        if (viewModel.isLoading) {
          return const Center(child: CircularProgressIndicator());
        }
        
        final user = viewModel.user;
        if (user == null) {
          return const Center(child: Text('找不到使用者'));
        }

        return Column(
          children: [
            Text(user.name),
            ElevatedButton(
              onPressed: () => viewModel.loadProfile(user.id),
              child: const Text('重新整理'),
            ),
          ],
        );
      },
    );
  }
}