flutter-architecting-apps
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseArchitecting 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调用、事件处理和数据同步。
- 组件: 将数据层严格划分为Repositories和Services。
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}');
},
);
}
}