mcpField
F
flutter-clean-arch

flutter-clean-arch

github.com/duckyman-ai开发1106 次安装19 周前更新
代码仓库

关于 flutter-clean-arch

flutter-clean-arch 是面向 Flutter 开发的技能,指导按特性优先的整洁架构构建应用。它结合 Riverpod 状态管理、fpdart 错误处理与 Dio + Retrofit 网络请求,并提供分层实践和参考指南。

主要功能

  1. 01特性优先的整洁架构
  2. 02Riverpod 状态管理与代码生成
  3. 03fpdart Either 错误处理
  4. 04Dio + Retrofit 类型安全请求
  5. 05数据层响应校验实践

适用场景

  1. 01规划 Flutter 应用的分层结构
  2. 02实现带状态管理的功能模块
  3. 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.md
error_handling.md
feature_examples.md
migration_guide.md
network_setup.md
presentation_layer.md
provider_patterns.md
quick_start.md
retrofit_patterns.md
testing_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 / .whenFreezed 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 类型进行函数式错误处理

参考资料

评论

支持 Markdown