flutter-architecting-apps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Architecting Flutter Applications

Flutter应用架构设计

Contents

目录

Core Architectural Principles

核心架构原则

Design Flutter applications to scale by strictly adhering to the following principles:
  • Enforce Separation of Concerns: Decouple UI rendering from business logic and data fetching. Organize the codebase into distinct layers (UI, Logic, Data) and further separate by feature within those layers.
  • Maintain a Single Source of Truth (SSOT): Centralize application state and data in the Data layer. Ensure the SSOT is the only component authorized to mutate its respective data.
  • Implement Unidirectional Data Flow (UDF): Flow state downwards from the Data layer to the UI layer. Flow events upwards from the UI layer to the Data layer.
  • Treat UI as a Function of State: Drive the UI entirely via immutable state objects. Rebuild widgets reactively when the underlying state changes.
通过严格遵循以下原则,设计具备可扩展性的Flutter应用:
  • 遵循关注点分离原则: 将UI渲染与业务逻辑、数据获取解耦。将代码库划分为不同的层级(UI层、逻辑层、数据层),并在各层级内按功能进一步拆分。
  • 保持单一数据源(SSOT): 将应用状态和数据集中在数据层。确保SSOT是唯一有权修改对应数据的组件。
  • 实现单向数据流(UDF): 状态从数据层向下流向UI层。事件从UI层向上流向数据层。
  • 将UI视为状态的函数: 完全通过不可变状态对象驱动UI。当底层状态变化时,响应式重建组件。

Structuring the Layers

分层结构设计

Separate the application into 2 to 3 distinct layers depending on complexity. Restrict communication so that a layer only interacts with the layer directly adjacent to it.
根据应用复杂度,将应用划分为2到3个不同的层级。限制层级间的通信,确保每个层级仅与直接相邻的层级交互。

1. UI Layer (Presentation)

1. UI层(展示层)

  • Views (Widgets): Build reusable, lean widgets. Strip all business and data-fetching logic from the widget tree. Restrict widget logic to UI-specific concerns (e.g., animations, routing, layout constraints).
  • ViewModels: Manage the UI state. Consume domain models from the Data/Logic layers and transform them into presentation-friendly formats. Expose state to the Views and handle user interaction events.
  • 视图(Widgets): 构建可复用、轻量化的Widgets。移除Widget树中所有业务逻辑和数据获取逻辑。Widget仅处理UI相关逻辑(如动画、路由、布局约束)。
  • ViewModels: 管理UI状态。从数据层/逻辑层获取领域模型,并将其转换为适合展示的格式。向视图暴露状态,并处理用户交互事件。

2. Logic Layer (Domain) - Conditional

2. 逻辑层(领域层)- 可选

  • If the application requires complex client-side business logic: Implement a Logic layer containing Use Cases or Interactors. Use this layer to orchestrate interactions between multiple repositories before passing data to the UI layer.
  • If the application is a standard CRUD app: Omit this layer. Allow ViewModels to interact directly with Repositories.
  • 如果应用需要复杂的客户端业务逻辑: 实现包含Use Cases或Interactors的逻辑层。使用该层协调多个Repository之间的交互,再将数据传递给UI层。
  • 如果应用是标准CRUD应用: 可省略该层。允许ViewModels直接与Repositories交互。

3. Data Layer (Model)

3. 数据层(模型层)

  • Responsibilities: Act as the SSOT for all application data. Handle business data, external API consumption, event processing, and data synchronization.
  • Components: Divide the Data layer strictly into Repositories and Services.
  • 职责: 作为所有应用数据的SSOT。处理业务数据、外部API调用、事件处理和数据同步。
  • 组件: 将数据层严格划分为RepositoriesServices

Implementing the Data Layer

数据层实现

Services

Services

  • Role: Wrap external APIs (HTTP servers, local databases, platform plugins).
  • Implementation: Write Services as stateless Dart classes. Do not store application state here.
  • Mapping: Create exactly one Service class per external data source.
  • 作用: 封装外部API(HTTP服务器、本地数据库、平台插件)。
  • 实现: 将Services编写为无状态Dart类。不要在此存储应用状态。
  • 映射: 每个外部数据源对应一个Service类。

Repositories

Repositories

  • Role: Act as the SSOT for domain data.
  • Implementation: Consume raw data from Services. Handle caching, offline synchronization, and retry logic.
  • Transformation: Transform raw API/Service data into clean Domain Models formatted for consumption by ViewModels.
  • 作用: 作为领域数据的SSOT。
  • 实现: 从Services获取原始数据。处理缓存、离线同步和重试逻辑。
  • 转换: 将原始API/Service数据转换为干净的领域模型,供ViewModels使用。

Feature Implementation Workflow

功能实现流程

Follow this sequential workflow when adding a new feature to the application.
Task Progress:
  • Step 1: Define Domain Models. Create immutable Dart classes representing the core data structures required by the feature.
  • Step 2: Implement Services. Create stateless Service classes to handle raw data fetching (e.g., HTTP GET/POST).
  • Step 3: Implement Repositories. Create Repository classes that consume the Services, handle caching, and return Domain Models.
  • Step 4: Implement ViewModels. Create ViewModels that consume the Repositories. Expose immutable state and define methods (commands) for user actions.
  • Step 5: Implement Views. Create Flutter Widgets that bind to the ViewModel state and trigger ViewModel methods on user interaction.
  • Step 6: Run Validator. Execute unit tests for Services, Repositories, and ViewModels. Execute widget tests for Views.
    • Feedback Loop: Review test failures -> Fix logic/mocking errors -> Re-run tests until passing.
添加新功能时,请遵循以下顺序流程。
任务进度:
  • 步骤1:定义领域模型。 创建不可变Dart类,代表功能所需的核心数据结构。
  • 步骤2:实现Services。 创建无状态Service类,处理原始数据获取(如HTTP GET/POST)。
  • 步骤3:实现Repositories。 创建Repository类,调用Services,处理缓存,并返回领域模型。
  • 步骤4:实现ViewModels。 创建ViewModels,调用Repositories。暴露不可变状态,并定义用户操作的方法(命令)。
  • 步骤5:实现视图。 创建Flutter Widgets,绑定到ViewModel状态,并在用户交互时触发ViewModel方法。
  • 步骤6:运行验证。 对Services、Repositories和ViewModels执行单元测试。对Views执行Widget测试。
    • 反馈循环: 查看测试失败结果 -> 修复逻辑/模拟错误 -> 重新运行测试直至通过。

Examples

示例

Data Layer: Service and Repository

数据层:Service和Repository

dart
// 1. Service (Stateless API Wrapper)
class UserApiService {
  final HttpClient _client;
  
  UserApiService(this._client);

  Future<Map<String, dynamic>> fetchUserRaw(String userId) async {
    final response = await _client.get('/users/$userId');
    return response.data;
  }
}

// 2. Domain Model (Immutable)
class User {
  final String id;
  final String name;
  
  const User({required this.id, required this.name});
}

// 3. Repository (SSOT & Data Transformer)
class UserRepository {
  final UserApiService _apiService;
  User? _cachedUser;

  UserRepository(this._apiService);

  Future<User> getUser(String userId) async {
    if (_cachedUser != null && _cachedUser!.id == userId) {
      return _cachedUser!;
    }
    
    final rawData = await _apiService.fetchUserRaw(userId);
    final user = User(id: rawData['id'], name: rawData['name']);
    
    _cachedUser = user; // Cache data
    return user;
  }
}
dart
// 1. Service (Stateless API Wrapper)
class UserApiService {
  final HttpClient _client;
  
  UserApiService(this._client);

  Future<Map<String, dynamic>> fetchUserRaw(String userId) async {
    final response = await _client.get('/users/$userId');
    return response.data;
  }
}

// 2. Domain Model (Immutable)
class User {
  final String id;
  final String name;
  
  const User({required this.id, required this.name});
}

// 3. Repository (SSOT & Data Transformer)
class UserRepository {
  final UserApiService _apiService;
  User? _cachedUser;

  UserRepository(this._apiService);

  Future<User> getUser(String userId) async {
    if (_cachedUser != null && _cachedUser!.id == userId) {
      return _cachedUser!;
    }
    
    final rawData = await _apiService.fetchUserRaw(userId);
    final user = User(id: rawData['id'], name: rawData['name']);
    
    _cachedUser = user; // Cache data
    return user;
  }
}

UI Layer: ViewModel and View

UI层:ViewModel和View

dart
// 4. ViewModel (State Management)
class UserViewModel extends ChangeNotifier {
  final UserRepository _userRepository;
  
  User? user;
  bool isLoading = false;
  String? error;

  UserViewModel(this._userRepository);

  Future<void> loadUser(String userId) async {
    isLoading = true;
    error = null;
    notifyListeners();

    try {
      user = await _userRepository.getUser(userId);
    } catch (e) {
      error = e.toString();
    } finally {
      isLoading = false;
      notifyListeners();
    }
  }
}

// 5. View (Lean UI)
class UserProfileView extends StatelessWidget {
  final UserViewModel viewModel;

  const UserProfileView({Key? key, required this.viewModel}) : super(key: key);

  
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: viewModel,
      builder: (context, child) {
        if (viewModel.isLoading) return const CircularProgressIndicator();
        if (viewModel.error != null) return Text('Error: ${viewModel.error}');
        if (viewModel.user == null) return const Text('No user data.');
        
        return Text('Hello, ${viewModel.user!.name}');
      },
    );
  }
}
dart
// 4. ViewModel (State Management)
class UserViewModel extends ChangeNotifier {
  final UserRepository _userRepository;
  
  User? user;
  bool isLoading = false;
  String? error;

  UserViewModel(this._userRepository);

  Future<void> loadUser(String userId) async {
    isLoading = true;
    error = null;
    notifyListeners();

    try {
      user = await _userRepository.getUser(userId);
    } catch (e) {
      error = e.toString();
    } finally {
      isLoading = false;
      notifyListeners();
    }
  }
}

// 5. View (Lean UI)
class UserProfileView extends StatelessWidget {
  final UserViewModel viewModel;

  const UserProfileView({Key? key, required this.viewModel}) : super(key: key);

  
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: viewModel,
      builder: (context, child) {
        if (viewModel.isLoading) return const CircularProgressIndicator();
        if (viewModel.error != null) return Text('Error: ${viewModel.error}');
        if (viewModel.user == null) return const Text('No user data.');
        
        return Text('Hello, ${viewModel.user!.name}');
      },
    );
  }
}