8.6 KiB
8.6 KiB
Airhub App 开发规范
所有新功能开发、代码修改、Bug 修复均需遵循本规范
一、项目架构
lib/
├── core/ # 基础设施层(全局共享)
│ ├── errors/ # 异常类 & Failure 类
│ ├── network/ # ApiClient, TokenManager, ApiConfig
│ ├── router/ # go_router 路由配置
│ └── services/ # 全局服务(LogCenter 等)
├── features/ # 功能模块(按业务领域划分)
│ ├── auth/ # 认证
│ ├── badge/ # 电子吧唧
│ ├── device/ # 设备管理
│ ├── notification/ # 通知
│ ├── spirit/ # 精灵/角色
│ ├── user/ # 用户
│ └── system/ # 系统(版本检查等)
├── pages/ # 独立页面(未归入 feature 的页面)
├── widgets/ # 全局可复用 Widget
├── theme/ # 主题、颜色、Design Tokens
├── services/ # 应用级服务(TTS、音乐生成等)
└── main.dart # 入口
Feature 模块内部结构
features/xxx/
├── data/
│ ├── datasources/ # 远程/本地数据源
│ ├── models/ # 数据模型(Freezed)
│ └── repositories/ # Repository 实现
├── domain/
│ └── repositories/ # Repository 接口
└── presentation/
├── controllers/ # Riverpod 控制器
├── pages/ # 页面
└── widgets/ # 功能专属 Widget
二、状态管理(Riverpod)
规则
- 使用
@riverpod注解声明 Provider,不手写StateNotifierProvider等 - Controller 使用
AsyncValue<T>管理异步状态 - Repository 返回
Either<Failure, T>(fpdart),Controller 内fold处理 - 页面中优先使用
ConsumerWidget(无状态);需要动画/本地状态时用ConsumerStatefulWidget ref.watch()用于 UI 绑定(触发重建),ref.read()用于事件处理(不触发重建)ref.listen()用于副作用(弹窗、导航、Toast),不触发 Widget 重建
示例
@riverpod
class MyController extends _$MyController {
@override
FutureOr<MyState> build() async {
final repo = ref.watch(myRepositoryProvider);
final result = await repo.fetchData();
return result.fold(
(failure) => throw failure,
(data) => MyState(data: data),
);
}
}
三、网络层
规则
- 所有 API 调用通过
ApiClient(Dio 封装),不直接使用http包或Dio() ApiClient自动处理:Token 附加、401 自动刷新、错误上报- 后端统一返回
{code, message, data},ApiClient._request()自动解析 - 异常类型:
ServerException(code != 0)、NetworkException(连接失败) - 不在 Widget/Controller 中直接调用 ApiClient,应通过 Repository 层
错误处理链
DataSource (throw Exception)
→ Repository (catch → Either<Failure, T>)
→ Controller (fold → AsyncValue)
→ UI (AsyncValue.when: loading/data/error)
四、路由(go_router)
规则
- 所有路由定义在
core/router/app_router.dart - 使用
context.go()进行声明式导航,避免Navigator.push()(页面内二级跳转除外) - 登录态检查在
redirect中统一处理 - 产品类型 → 路由映射在
_productRoutesMap 中维护
五、性能规范
内存管理
-
所有 Stream 订阅必须存入变量,dispose 时取消
// ✅ 正确 StreamSubscription<Duration>? _positionSub; _positionSub = stream.listen((data) { ... }); @override void dispose() { _positionSub?.cancel(); super.dispose(); } // ❌ 错误 — 无法取消,造成内存泄漏 stream.listen((data) { ... }); -
AnimationController、VideoPlayerController、AudioPlayer 等必须在 dispose 中释放
-
页面离开时停止 BLE 扫描
@override void dispose() { FlutterBluePlus.stopScan(); _scanSubscription?.cancel(); super.dispose(); }
Widget 重建优化
-
能用
const的地方必须加const// ✅ const SizedBox(height: 16) const EdgeInsets.all(16) const Text('Hello') // ❌ SizedBox(height: 16) EdgeInsets.all(16) -
动画区域用
RepaintBoundary包裹,防止全页面重绘RepaintBoundary( child: AnimatedGradientBackground(), ) -
AnimatedBuilder必须使用child参数,将不变的子树传入 child,不要在 builder 中重建// ✅ AnimatedBuilder( animation: _controller, child: const HeavyWidget(), // 只构建一次 builder: (context, child) => Transform.rotate( angle: _controller.value, child: child, // 复用 ), )
API 调用
-
多个独立 API 调用使用
Future.wait并行,不要顺序 await// ✅ 并行 final results = await Future.wait([ api.get('/shelves/'), api.get('/stories/'), ]); // ❌ 顺序(慢 N 倍) final shelves = await api.get('/shelves/'); final stories = await api.get('/stories/'); -
耗时计算(JSON 解析、图片处理)放到 Isolate
final result = await Isolate.run(() => jsonDecode(bigJson));
图片
- 网络图片使用
Image.network+fit: BoxFit.cover,不要手动指定cacheWidth/cacheHeight(会导致显示异常) - 大量图片列表使用
ListView.builder,不要ListView(children: [...])
六、构建配置
Android Release
build.gradle.kts中 release 配置:isMinifyEnabled = true— R8 代码压缩isShrinkResources = true— 移除未使用资源- ProGuard 规则在
proguard-rules.pro维护
- 打包命令:
flutter build apk --release --split-per-abi --obfuscate --split-debug-info=./debug-info
iOS Release
- 构建命令:
flutter build ipa --release --obfuscate --split-debug-info=./debug-info
七、命名规范
文件命名
| 类型 | 格式 | 示例 |
|---|---|---|
| 页面 | xxx_page.dart |
login_page.dart |
| 控制器 | xxx_controller.dart |
auth_controller.dart |
| 数据源 | xxx_remote_data_source.dart |
spirit_remote_data_source.dart |
| 仓库实现 | xxx_repository_impl.dart |
auth_repository_impl.dart |
| 仓库接口 | xxx_repository.dart |
auth_repository.dart |
| 模型 | xxx_model.dart |
user_model.dart |
| Widget | xxx_widget.dart 或直接描述 |
gradient_button.dart |
| 生成文件 | *.g.dart / *.freezed.dart |
自动生成,不手动编辑 |
类命名
- PascalCase:
AuthController、DeviceControlPage、ServerException - 私有前缀:
_buildHeader()、_positionSub
变量命名
- camelCase:
currentUser、isPlaying、_audioPlayer - Provider:
xxxProvider(由@riverpod自动生成)
八、主题系统
颜色
- 全局颜色定义在
theme/app_colors.dart - 产品主题颜色在
theme/product_theme.dart(4 种产品类型) - 不要在 Widget 中硬编码颜色值,优先使用
AppColors.xxx或Theme.of(context)
字体
- 主字体:DM Sans(正文)、Outfit(标题)、Press Start 2P(品牌像素风)
- 通过
google_fonts包引用,运行时自动下载并缓存 - 本地字体:Inter(在
pubspec.yaml中声明)
间距
- 使用
AppSpacing.xs/sm/md/lg/xl常量 - 不要在 Widget 中硬编码间距数值
九、Git 规范
分支命名
- 功能分支:
fix/auto-YYYYMMDD-HHMMSS - 合并目标:
main
Commit 规范
fix:— Bug 修复feat:— 新功能refactor:— 重构(不改变行为)chore:— 构建配置、依赖更新等
十、检查清单(PR 提交前)
dart analyze无 Error(Warning/Info 可接受)- 所有 Stream 订阅已在 dispose 中取消
- 所有 Controller/Player 已在 dispose 中释放
- 多个独立 API 调用已用
Future.wait并行化 - 动画 Widget 使用了
RepaintBoundary或AnimatedBuilder(child:) - 新增的 Widget 构造函数尽可能标记为
const - 列表使用
ListView.builder,而非ListView(children:) - 未引入新的硬编码颜色值(使用 AppColors 或 theme)
- Android release 构建正常(minify + shrinkResources 不会崩溃)