Feature 开发工作流
完整的 Feature 开发流程,确保代码分层清晰、UI 无硬编码。
🔄 工作流程图
┌─────────────────────────────────────────────────────────────────────────────┐ │ Phase 0: 需求分析 │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 文字描述 │ │ UI 截图 │ │ 设计稿 │ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ └────────────────┼────────────────┘ │ │ ▼ │ │ ┌───────────────────────┐ │ │ │ 提取: 实体 / API / UI │ │ │ └───────────┬───────────┘ │ └──────────────────────────┼──────────────────────────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ Phase 1-4: 分层开发 │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Domain │───▶│ Data │───▶│Provider │───▶│ UI │───▶│ Route │ │ │ │ 实体 │ │ 数据源 │ │ 状态管理 │ │ 页面 │ │ 路由 │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ │ │ [检查点] [检查点] [检查点] [检查点] [检查点] │ └──────────────────────────┬──────────────────────────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ Phase 5: 质量检查 │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ analyze │ │ format │ │ test │ │ l10n │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ 参考: .claude/skills/code-quality │ └─────────────────────────────────────────────────────────────────────────────┘
📋 Phase 0: 需求分析
输入类型
输入 分析要点
文字描述 提取功能点、业务规则、数据流向
UI 截图 识别组件结构、交互方式、状态变化
设计稿 提取颜色/字体(映射到 Theme)、间距、组件层级
分析输出
需求分析结果
1. 实体定义
- 实体名称: User
- 字段: id, name, email, avatar
- 关联: UserRole (可选)
2. API 接口
- GET /users - 获取用户列表
- GET /users/:id - 获取用户详情
- POST /users - 创建用户
3. UI 组件
- UserListPage: 列表页面
- UserListItem: 列表项组件
- UserDetailPage: 详情页面
4. 状态流转
- Initial → Loading → Loaded/Error
- 支持下拉刷新、分页加载
5. 国际化文本
- userListTitle: 用户列表
- userDetailTitle: 用户详情
- emptyList: 暂无用户
Phase 0 检查清单
检查项 状态
☐ 实体字段已明确
☐ API 接口已确认(或 mock 方案)
☐ UI 组件层级已拆分
☐ 状态流转已定义
☐ 国际化 key 已规划
🚫 核心原则:UI 层禁止硬编码
禁止项
// ❌ 禁止:硬编码文本 Text('用户列表')
// ❌ 禁止:硬编码颜色/尺寸 Container(color: Color(0xFF2196F3), padding: EdgeInsets.all(16))
// ❌ 禁止:模拟数据 final users = [User(name: 'Test'), User(name: 'Demo')];
// ❌ 禁止:魔法数字 SizedBox(height: 24)
正确做法
// ✅ 国际化文本 Text(context.l10n.userListTitle)
// ✅ 主题颜色/间距 Container( color: Theme.of(context).colorScheme.primary, padding: const EdgeInsets.all(AppSpacing.md), )
// ✅ 从 Provider 获取数据 final users = ref.watch(userListProvider);
// ✅ 命名常量 SizedBox(height: AppSpacing.lg)
📁 开发顺序(自底向上)
Step 1: Domain 层(纯 Dart)
目的:定义业务实体和仓库接口
lib/features/<name>/domain/ ├── entities/ │ └── <name>.dart # 业务实体 └── repositories/ └── <name>_repository.dart # 仓库接口
实体模板:
// domain/entities/user.dart class User { const User({ required this.id, required this.name, required this.email, });
final String id; final String name; final String email;
User copyWith({String? id, String? name, String? email}) { return User( id: id ?? this.id, name: name ?? this.name, email: email ?? this.email, ); }
@override bool operator ==(Object other) => identical(this, other) || other is User && id == other.id;
@override int get hashCode => id.hashCode; }
仓库接口模板:
// domain/repositories/user_repository.dart import '../entities/user.dart'; import '../../../../core/utils/result.dart';
abstract class UserRepository { Future<Result<List<User>>> getUsers(); Future<Result<User>> getUserById(String id); Future<Result<void>> saveUser(User user); }
Step 2: Data 层
目的:实现数据源和仓库
lib/features/<name>/data/ ├── datasources/ │ ├── <name>_remote_data_source.dart # 网络数据源 │ └── <name>_local_data_source.dart # 本地数据源 ├── models/ │ └── <name>_dto.dart # 数据传输对象 └── repositories/ └── <name>_repository_impl.dart # 仓库实现
远程数据源模板:
// data/datasources/user_remote_data_source.dart import '../../../../core/network/dio_client.dart'; import '../models/user_dto.dart';
abstract class UserRemoteDataSource { Future<List<UserDto>> getUsers(); Future<UserDto> getUserById(String id); }
class UserRemoteDataSourceImpl implements UserRemoteDataSource { UserRemoteDataSourceImpl({required this.dioClient});
final DioClient dioClient;
@override Future<List<UserDto>> getUsers() async { final response = await dioClient.get('/users'); return (response.data as List) .map((json) => UserDto.fromJson(json)) .toList(); }
@override Future<UserDto> getUserById(String id) async { final response = await dioClient.get('/users/$id'); return UserDto.fromJson(response.data); } }
DTO 模板:
// data/models/user_dto.dart import '../../domain/entities/user.dart';
class UserDto { UserDto({required this.id, required this.name, required this.email});
factory UserDto.fromJson(Map<String, dynamic> json) { return UserDto( id: json['id'] as String, name: json['name'] as String, email: json['email'] as String, ); }
final String id; final String name; final String email;
Map<String, dynamic> toJson() => {'id': id, 'name': name, 'email': email};
User toEntity() => User(id: id, name: name, email: email); }
仓库实现模板:
// data/repositories/user_repository_impl.dart import '../../../../core/error/error_mapper.dart'; import '../../../../core/utils/result.dart'; import '../../domain/entities/user.dart'; import '../../domain/repositories/user_repository.dart'; import '../datasources/user_remote_data_source.dart';
class UserRepositoryImpl implements UserRepository { UserRepositoryImpl({required this.remoteDataSource});
final UserRemoteDataSource remoteDataSource;
@override Future<Result<List<User>>> getUsers() async { try { final dtos = await remoteDataSource.getUsers(); return Success(dtos.map((dto) => dto.toEntity()).toList()); } catch (e) { return Err(ErrorMapper.mapException(e)); } }
@override Future<Result<User>> getUserById(String id) async { try { final dto = await remoteDataSource.getUserById(id); return Success(dto.toEntity()); } catch (e) { return Err(ErrorMapper.mapException(e)); } }
@override Future<Result<void>> saveUser(User user) async { // 实现保存逻辑 return const Success(null); } }
Step 3: Presentation 层 - Provider
目的:状态管理和业务逻辑
lib/features/<name>/presentation/ └── providers/ └── <name>_provider.dart
Provider 模板(异步数据):
// presentation/providers/user_provider.dart import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/di.dart'; import '../../data/datasources/user_remote_data_source.dart'; import '../../data/repositories/user_repository_impl.dart'; import '../../domain/entities/user.dart'; import '../../domain/repositories/user_repository.dart';
// 数据源 Provider final userRemoteDataSourceProvider = Provider<UserRemoteDataSource>((ref) { return UserRemoteDataSourceImpl(dioClient: ref.watch(dioClientProvider)); });
// 仓库 Provider final userRepositoryProvider = Provider<UserRepository>((ref) { return UserRepositoryImpl( remoteDataSource: ref.watch(userRemoteDataSourceProvider), ); });
// 状态定义 sealed class UserListState { const UserListState(); }
class UserListInitial extends UserListState { const UserListInitial(); }
class UserListLoading extends UserListState { const UserListLoading(); }
class UserListLoaded extends UserListState { const UserListLoaded(this.users); final List<User> users; }
class UserListError extends UserListState { const UserListError(this.message); final String message; }
// Controller final userListControllerProvider = NotifierProvider<UserListController, UserListState>( UserListController.new, );
class UserListController extends Notifier<UserListState> { @override UserListState build() { // 初始化时加载数据 Future.microtask(loadUsers); return const UserListLoading(); }
UserRepository get _repository => ref.read(userRepositoryProvider);
Future<void> loadUsers() async { state = const UserListLoading(); final result = await _repository.getUsers(); result.when( success: (users) => state = UserListLoaded(users), failure: (failure) => state = UserListError(failure.message), ); }
Future<void> refresh() async { await loadUsers(); } }
Step 4: Presentation 层 - UI
目的:纯 UI 展示,无业务逻辑
lib/features/<name>/presentation/ ├── pages/ │ └── <name>_page.dart # 页面容器 └── widgets/ └── <name>_view.dart # 视图组件
Page 模板:
// presentation/pages/user_list_page.dart import 'package:flutter/material.dart';
import '../../../../core/l10n/l10n.dart'; import '../../../../core/widgets/app_scaffold.dart'; import '../widgets/user_list_view.dart';
class UserListPage extends StatelessWidget { const UserListPage({super.key});
@override Widget build(BuildContext context) { return AppScaffold( appBar: AppBar(title: Text(context.l10n.userListTitle)), body: const UserListView(), ); } }
View 模板(处理状态):
// presentation/widgets/user_list_view.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/l10n/l10n.dart'; import '../../../../core/widgets/error_view.dart'; import '../../../../core/widgets/loading_indicator.dart'; import '../providers/user_provider.dart'; import 'user_list_item.dart';
class UserListView extends ConsumerWidget { const UserListView({super.key});
@override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(userListControllerProvider);
return switch (state) {
UserListInitial() => const SizedBox.shrink(),
UserListLoading() => const LoadingIndicator(),
UserListError(:final message) => ErrorView(
message: message,
onRetry: () => ref.read(userListControllerProvider.notifier).refresh(),
),
UserListLoaded(:final users) => users.isEmpty
? Center(child: Text(context.l10n.emptyList))
: RefreshIndicator(
onRefresh: () =>
ref.read(userListControllerProvider.notifier).refresh(),
child: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) => UserListItem(user: users[index]),
),
),
};
} }
Item 模板:
// presentation/widgets/user_list_item.dart import 'package:flutter/material.dart';
import '../../domain/entities/user.dart';
class UserListItem extends StatelessWidget { const UserListItem({super.key, required this.user});
final User user;
@override Widget build(BuildContext context) { final theme = Theme.of(context);
return ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.primaryContainer,
child: Text(
user.name.isNotEmpty ? user.name[0].toUpperCase() : '?',
style: TextStyle(color: theme.colorScheme.onPrimaryContainer),
),
),
title: Text(user.name, style: theme.textTheme.titleMedium),
subtitle: Text(user.email, style: theme.textTheme.bodySmall),
);
} }
Step 5: 路由配置
// presentation/routes.dart import 'package:go_router/go_router.dart';
import 'pages/user_list_page.dart';
class UserRoutes { UserRoutes._();
static const String userList = '/users'; static const String userDetail = '/users/:id'; }
List<GoRoute> buildUserRoutes() => [ GoRoute( path: UserRoutes.userList, builder: (context, state) => const UserListPage(), ), ];
注册到 app/router.dart:
import '../features/user/presentation/routes.dart';
final routerProvider = Provider<GoRouter>((ref) => GoRouter( routes: [ ...buildUserRoutes(), // 其他路由... ], ));
Step 6: 国际化
添加到 l10n/app_en.arb:
{ "userListTitle": "Users", "emptyList": "No data available" }
添加到 l10n/app_zh.arb:
{ "userListTitle": "用户列表", "emptyList": "暂无数据" }
生成:
flutter gen-l10n
✅ 各阶段检查清单
Phase 1: Domain 检查点
检查项 状态
☐ 实体类使用 const 构造函数
☐ 所有字段使用 final
☐ 实现 copyWith 方法
☐ 重写 == 和 hashCode
☐ 仓库接口返回 Result<T>
☐ 无 Flutter 依赖(纯 Dart)
Phase 2: Data 检查点
检查项 状态
☐ DTO 与 Entity 分离
☐ fromJson / toJson 实现完整
☐ toEntity() 转换方法
☐ 数据源接口 + 实现分离
☐ 异常捕获并转换为 Failure
☐ 使用 ErrorMapper.mapException()
Phase 3: Provider 检查点
检查项 状态
☐ 状态使用 sealed class 定义
☐ 包含 Initial/Loading/Loaded/Error 状态
☐ Controller 继承 Notifier 或 AsyncNotifier
☐ 数据加载在 Controller 中完成
☐ Provider 依赖链正确(DataSource → Repository → Controller)
Phase 4: UI 检查点
检查项 状态
☐ 文本使用 context.l10n.xxx (无硬编码)
☐ 颜色使用 Theme.of(context) (无硬编码)
☐ 间距使用命名常量(无魔法数字)
☐ 数据来自 Provider(无模拟数据)
☐ Page 与 View/Item 组件分离
☐ 使用 switch 表达式处理状态
☐ Loading/Error/Empty 状态 UI 完整
☐ 使用 const 构造函数
Phase 4.5: Route & L10n 检查点
检查项 状态
☐ 路由常量定义在 routes.dart
☐ buildXxxRoutes() 函数已导出
☐ 路由已注册到 app/router.dart
☐ 国际化 key 已添加到 app_en.arb
☐ 国际化 key 已添加到 app_zh.arb
☐ 已运行 flutter gen-l10n
🔍 Phase 5: 质量检查
参考: .claude/skills/code-quality/SKILL.md
执行命令
1. 代码分析(必须通过)
flutter analyze --fatal-infos
2. 格式检查(必须通过)
dart format --set-exit-if-changed .
3. 运行测试(必须通过)
flutter test test/features/<name>/
4. 生成国际化(如有变更)
flutter gen-l10n
5. 依赖检查(建议)
flutter pub outdated
Phase 5 检查清单
5.1 静态分析
检查项 命令 状态
☐ 无 analyze 错误 flutter analyze
☐ 无 analyze 警告 flutter analyze --fatal-infos
☐ 代码格式正确 dart format --set-exit-if-changed .
5.2 测试覆盖
检查项 状态
☐ Domain 层单元测试
☐ Provider/Controller 测试
☐ 测试全部通过
5.3 安全检查
检查项 状态
☐ 无硬编码 API 密钥/Token
☐ 无硬编码密码/Secret
☐ 敏感数据使用 SecureStorage
☐ 网络请求使用 HTTPS
☐ 无敏感信息在日志中输出
5.4 性能检查
检查项 标准 状态
☐ 单文件行数 < 500 行
☐ Widget 嵌套层级 < 10 层
☐ 列表使用 ListView.builder
☐ 使用 const 构造函数
☐ 避免在 build 中创建大对象
5.5 代码规范
检查项 状态
☐ 文件命名 snake_case
☐ 类命名 PascalCase
☐ 私有成员 _ 前缀
☐ 导入语句已排序
☐ 无未使用的导入/变量
质量检查自动化(推荐)
使用子代理执行完整质量检查:
Task({ subagent_type: 'general-purpose', description: '运行 Feature 质量检查', prompt: ` 对 lib/features/<name>/ 执行完整质量检查:
- flutter analyze lib/features/<name>/
- dart format --set-exit-if-changed lib/features/<name>/
- flutter test test/features/<name>/
如有错误,分析并修复,再次验证直到全部通过。 返回检查结果摘要。
遵循 .claude/skills/code-quality/SKILL.md 中的规范。 `, })
📋 完整检查清单汇总
阶段 核心检查项
Phase 0 需求分析完整(实体/API/UI/状态/L10n)
Phase 1 Domain 纯 Dart,immutable 实体
Phase 2 Data DTO 分离,异常转 Failure
Phase 3 Provider sealed class 状态
Phase 4 UI 无硬编码,数据来自 Provider
Phase 4.5 路由注册,国际化完成
Phase 5 analyze + format + test 全通过
🔧 常用命令速查
开发流程
flutter pub get # 获取依赖 flutter gen-l10n # 生成国际化
质量检查
flutter analyze # 代码分析 flutter analyze lib/features/<name>/ # 分析指定 feature dart format . # 格式化 dart format lib/features/<name>/ # 格式化指定 feature
测试
flutter test # 全部测试 flutter test test/features/<name>/ # Feature 测试 flutter test --coverage # 覆盖率报告
依赖
flutter pub outdated # 检查过期依赖 flutter pub upgrade # 升级依赖