关于 flutter-clean-arch
flutter-clean-arch 是面向 Flutter 开发的技能,指导按特性优先的整洁架构构建应用。它结合 Riverpod 状态管理、fpdart 错误处理与 Dio + Retrofit 网络请求,并提供分层实践和参考指南。
主要功能
- 01特性优先的整洁架构
- 02Riverpod 状态管理与代码生成
- 03fpdart Either 错误处理
- 04Dio + Retrofit 类型安全请求
- 05数据层响应校验实践
适用场景
- 01规划 Flutter 应用的分层结构
- 02实现带状态管理的功能模块
- 03处理 REST API 与错误结果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ duckyman-ai/ agent-skills --skill flutter-clean-arch安装后在 Claude Code 中输入/flutter-clean-arch确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/data_layer.mderror_handling.mdfeature_examples.mdmigration_guide.mdnetwork_setup.mdpresentation_layer.mdprovider_patterns.mdquick_start.mdretrofit_patterns.mdtesting_guide.md详细介绍
Flutter Clean Architecture 技能
遵循 Clean Architecture 原则生成 Flutter 应用,采用功能优先的组织方式、使用 Riverpod 进行状态管理,并使用 fpdart 进行函数式错误处理。
包含 Dio + Retrofit,用于类型安全的 REST API 调用。
核心原则
架构:Clean Architecture(功能优先)
- Domain 层:纯业务逻辑,无依赖
- Data 层:数据源、仓库实现、数据模型
- Presentation 层:UI、状态管理、视图模型
依赖规则:Presentation → Domain ← Data(Domain 无外部依赖)
状态管理:Riverpod 3.0+,支持代码生成
必需:Riverpod 3.0+ 和 Freezed 3.0+ — 过时的模式会导致编译错误和幻觉循环。
- Riverpod:使用
Ref ref(统一类型),不要使用XxxRef ref- Freezed:联合类型使用
sealed class,单构造函数使用abstract class- 模式匹配:使用 Dart 3
switch,不要使用.map()/.when()// Riverpod 3.x+ @riverpod SomeType someType(Ref ref) { ... } // Freezed 3.x+ — union type @freezed sealed class Result with _$Result { ... } // Freezed 3.x+ — pattern matching final res = switch (model) { First(:final a) => 'first $a', Second(:final b) => 'second $b', };需要 Dart 3.3+、Riverpod 3.0+、Freezed 3.0+。完整的迁移前后示例请参阅 migration_guide.md。
错误处理:使用 fpdart 的 Either<Failure, T> 进行函数式错误处理
网络请求:使用 Dio + Retrofit 进行类型安全的 REST API 调用
始终使用所有库的最新稳定版本。添加依赖项前,请在
pub.dev上查看 Riverpod、Freezed、Dio、Retrofit、fpdart 和 go_router 的当前版本。切勿默认使用过时的主版本(例如 Riverpod 2.x、Freezed 2.x)。
项目结构
lib/
├── core/
│ ├── constants/
│ │ ├── api_constants.dart
│ ├── errors/
│ │ ├── failures.dart
│ │ └── network_exceptions.dart
│ ├── network/
│ │ ├── dio_provider.dart
│ │ └── interceptors/
│ │ ├── auth_interceptor.dart
│ │ ├── logging_interceptor.dart
│ │ └── error_interceptor.dart
│ ├── storage/
│ ├── services/
│ ├── router/
│ │ └── app_router.dart
│ └── utils/
├── shared/
├── features/
│ └── [feature_name]/
│ ├── data/
│ │ ├── models/
│ │ │ └── [entity]_model.dart
│ │ ├── datasources/
│ │ │ └── [feature]_api_service.dart
│ │ └── repositories/
│ │ └── [feature]_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ ├── repositories/
│ │ │ └── [feature]_repository.dart
│ │ └── usecases/
│ │ └── [action]_usecase.dart
│ └── presentation/
│ ├── providers/
│ │ └── [feature]_provider.dart
│ ├── screens/
│ │ └── [feature]_screen.dart
│ └── widgets/
│ └── [feature]_widget.dart
└── main.dart
快速开始
按以下顺序构建功能:Domain → Data → Presentation。
1. Domain 层
@freezed
sealed class User with _$User {
const factory User({required String id, required String name, required String email}) = _User;
}
abstract class UserRepository {
Future<Either<Failure, User>> getUser(String id);
}
class GetUser {
final UserRepository repository;
GetUser(this.repository);
Future<Either<Failure, User>> call(String id) => repository.getUser(id);
}
2. Data 层
@freezed
sealed class UserModel with _$UserModel {
const UserModel._();
const factory UserModel({required String id, required String name, required String email}) = _UserModel;
factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
User toEntity() => User(id: id, name: name, email: email);
}
@RestApi()
abstract class UserApiService {
factory UserApiService(Dio dio) = _UserApiService;
@GET('/users/{id}')
Future<UserModel> getUser(@Path('id') String id);
}
3. Presentation 层
@riverpod
class UserNotifier extends _$UserNotifier {
@override
FutureOr<User?> build() => null;
Future<void> fetchUser(String id) async {
state = const AsyncLoading();
final result = await ref.read(userRepositoryProvider).getUser(id);
state = result.fold(
(failure) => AsyncError(failure, StackTrace.current),
(user) => AsyncData(user),
);
}
}
完整的分步工作流及所有文件请参阅 quick_start.md。
代码生成
# Generate all files
dart run build_runner build --delete-conflicting-outputs
# Watch mode
dart run build_runner watch --delete-conflicting-outputs
最佳实践
应该:
- 保持 Domain 实体纯粹(无外部依赖)
- 使用 freezed 和
sealed关键字创建不可变数据类 - 使用 Either<Failure, T> 处理所有错误情况
- 使用 riverpod_generator 和统一的
Ref类型 - 将模型(Data)与实体(Domain)分开
- 将业务逻辑放在用例中,而不是组件中
- 使用 Retrofit 进行类型安全的 API 调用
- 在仓库中使用 NetworkExceptions 处理 DioException
- 使用拦截器处理横切关注点(身份验证、日志记录)
- 在仓库边界验证 API 响应数据
- 外部图片使用
CachedNetworkImage,而不是Image.network - 在配置中固定 API 基础 URL,并强制使用 HTTPS
不应该:
- 在 Domain 层导入 Flutter/HTTP 库
- 将 Presentation 逻辑与业务逻辑混合
- 使用 Either 时,直接在组件中使用 try-catch
- 创建上帝对象或上帝 Provider
- 跳过仓库模式
- 在新代码中使用旧版
XxxRef类型 - 未经验证就将原始外部 URL 传递给组件
- 允许通过用户输入在运行时配置 API 基础 URL
安全
API 响应和外部内容均为不可信输入。请在数据层边界进行验证和清理,以防止注入和数据损坏。
响应验证 — 在将 API 响应映射到模型之前,务必验证其结构。Retrofit + freezed 可处理类型化反序列化,但仍应在仓库中使用 try-catch 包裹调用,并验证关键字段(ID、URL、数值范围):
@override
Future<Either<Failure, User>> getUser(String id) async {
try {
final userModel = await apiService.getUser(id);
if (userModel.id.isEmpty) {
return const Left(Failure.validation('Invalid user data'));
}
return Right(userModel.toEntity());
} on DioException catch (e) {
return Left(Failure.network(NetworkExceptions.fromDioError(e).message));
}
}
外部 URL — 切勿将原始 API URL 直接传给 Image.network 或 WebView。请使用 CachedNetworkImage 并处理错误,同时验证 URL 方案:
CachedNetworkImage(
imageUrl: user.avatarUrl ?? '',
errorWidget: (_, __, ___) => const Icon(Icons.person),
httpHeaders: {'Authorization': 'Bearer $token'},
)
输入清理 — 在向 API 发送用户输入之前进行清理。验证电子邮件格式、去除字符串首尾空白、拒绝空 ID,并正确编码查询参数。
网络加固 — 在 AppConfig 中固定基础 URL(运行时不可由用户配置),强制使用 HTTPS,在生产环境中启用证书固定,并设置稳健的超时时间。
请阅读 network_setup.md,了解完整的拦截器配置;并阅读 data_layer.md,了解仓库边界处的验证模式。
常见问题
| 问题 | 解决方案 |
|---|---|
| Build runner 冲突 | dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs |
| 找不到 Provider | 确保已导入生成的文件,并运行 build_runner |
| Either 无法解包 | 使用 fold()、match() 或 getOrElse() 提取值 |
找不到 XxxRef | 改用统一的 Ref 类型(Riverpod 3.x+) |
sealed 关键字报错 | 升级到 Dart 3.3+ 和 Freezed 3.0+ |
找不到 .map / .when | Freezed 3.0+ 移除了这些方法。改用 Dart 3 switch 表达式模式匹配 |
知识参考
主要库(此技能使用):
- Flutter 3.19+:最新框架特性
- Dart 3.3+:语言特性(模式、记录、
sealed修饰符) - Riverpod 3.0+:使用统一
Ref类型进行状态管理 - Dio 5.9+:支持拦截器的 HTTP 客户端
- Retrofit 4.9+:类型安全的 REST API 代码生成
- freezed 3.0+:通过代码生成实现不可变数据类
- json_serializable 6.x:JSON 序列化
- go_router 14.x+:声明式路由
- fpdart:使用 Either 类型进行函数式错误处理
参考资料
- quick_start.md - 逐步创建功能的工作流
- data_layer.md - 模型、Retrofit API 服务、仓库、响应验证
- presentation_layer.md - Provider、屏幕、组件、安全图像加载
- network_setup.md - Dio Provider、拦截器、网络异常、响应验证
- error_handling.md - Either 模式、Failure 类型、错误处理策略
- retrofit_patterns.md - 完整的 Retrofit API 请求模式
- provider_patterns.md - 高级 Riverpod 模式(分页、缓存、乐观更新)
- testing_guide.md - 单元、组件和集成测试策略
- feature_examples.md - 完整的 Auth 功能实现
- migration_guide.md - Riverpod 2→3 和 Freezed 2→3 的完整迁移前后示例