flutter-apply-architecture-best-practices

flutter-apply-architecture-best-practices

熱門

使用官方推薦的分層架構(介面層、邏輯層、資料層)來設計 Flutter 應用程式架構。適合在規劃新專案結構或進行擴充性重構時使用。

2783星標
163分支
更新於 2026/8/5
SKILL.md
唯讀
名稱
flutter-apply-architecture-best-practices
描述

使用官方推薦的分層架構(介面層、邏輯層、資料層)來設計 Flutter 應用程式架構。適合在規劃新專案結構或進行擴充性重構時使用。

Flutter 應用程式架構設計

目錄

架構分層

透過將應用程式劃分為明確的層級,嚴格執行「關注點分離」(Separation of Concerns)。切勿將 UI 渲染與商業邏輯或資料擷取混在一起。

UI 層(展示層)

採用 MVVM(Model-View-ViewModel)模式管理 UI 狀態與邏輯。

  • Views: 撰寫可複用且精簡的 Widget。View 中的邏輯應僅限於 UI 專屬操作(如動畫、版面限制、簡單路由)。所有所需資料皆由 ViewModel 傳入。
  • ViewModels: 管理 UI 狀態並處理使用者互動。繼承 ChangeNotifier(或使用 Listenable)來對外暴露狀態。提供不可變的狀態快照給 View。透過建構子將 Repository 注入至 ViewModel 中。

資料層

實作 Repository 模式以隔離資料存取邏輯,建立單一事實來源(Single Source of Truth)。

  • Services: 建立無狀態(stateless)類別來封裝外部 API(HTTP 用戶端、本機資料庫、平台外掛程式)。回傳原始 API 模型或 Result 包裝物件。
  • Repositories: 整合一個或多個 Service。將原始 API 模型轉換為乾淨的 Domain Model(領域模型)。負責處理快取、離線同步與重試邏輯,並將 Domain Model 暴露給 ViewModel。

邏輯層(Domain 領域層 - 可選)

  • Use Cases: 僅在應用程式包含複雜商業邏輯(避免 ViewModel 過於臃腫),或是邏輯需要在多個 ViewModel 之間複用時才需要實作此層。將這些邏輯抽離為獨立的 Use Case(Interactor)類別,介於 ViewModel 與 Repository 之間。

專案結構

採用混合式結構整理程式碼庫:UI 元件依功能(Feature)分組,Data 與 Domain 元件則依類型分組。

lib/
├── data/
│   ├── models/         # API 模型
│   ├── repositories/   # Repository 實作
│   └── services/       # API 用戶端、本機儲存封裝
├── domain/
│   ├── models/         # 乾淨的領域模型 (Domain models)
│   └── use_cases/      # 可選的商業邏輯類別
└── ui/
    ├── core/           # 共用 Widget、主題、字型排版
    └── features/
        └── [feature_name]/
            ├── view_models/
            └── views/

工作流程:開發新功能

在應用程式中新增功能時,請遵循以下步驟流程。可複製此核取清單以追蹤開發進度。

任務進度

  • [ ] 步驟 1:定義 Domain Model。 使用 freezedbuilt_value 為該功能建立不可變(immutable)的資料類別。
  • [ ] 步驟 2:實作 Service。 建立或更新 Service 類別以處理外部 API 通訊。
  • [ ] 步驟 3:實作 Repository。 建立 Repository 來呼叫 Service 並回傳 Domain Model。
  • [ ] 步驟 4:套用條件邏輯(Domain 層)。
    • 若該功能需要複雜的資料轉換或跨 Repository 邏輯: 建立 Use Case 類別。
    • 若該功能僅為簡單的 CRUD 操作: 略過此步驟,直接進入步驟 5。
  • [ ] 步驟 5:實作 ViewModel。 建立繼承自 ChangeNotifier 的 ViewModel,注入所需的 Repository/Use Case,並暴露不可變狀態與操作方法。
  • [ ] 步驟 6:實作 View。 建立 UI Widget,使用 ListenableBuilderAnimatedBuilder 監聽 ViewModel 的變更。
  • [ ] 步驟 7:注入依賴(Dependency Injection)。 在依賴注入容器(如 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(單一事實來源,回傳 Domain Model)
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); // 轉換為 Domain Model
    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('User not found'));
        }

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