# 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) ### 规则 1. **使用 `@riverpod` 注解**声明 Provider,不手写 `StateNotifierProvider` 等 2. Controller 使用 `AsyncValue` 管理异步状态 3. Repository 返回 `Either`(fpdart),Controller 内 `fold` 处理 4. **页面中优先使用 `ConsumerWidget`**(无状态);需要动画/本地状态时用 `ConsumerStatefulWidget` 5. **`ref.watch()` 用于 UI 绑定**(触发重建),**`ref.read()` 用于事件处理**(不触发重建) 6. **`ref.listen()` 用于副作用**(弹窗、导航、Toast),不触发 Widget 重建 ### 示例 ```dart @riverpod class MyController extends _$MyController { @override FutureOr build() async { final repo = ref.watch(myRepositoryProvider); final result = await repo.fetchData(); return result.fold( (failure) => throw failure, (data) => MyState(data: data), ); } } ``` --- ## 三、网络层 ### 规则 1. 所有 API 调用通过 `ApiClient`(Dio 封装),不直接使用 `http` 包或 `Dio()` 2. `ApiClient` 自动处理:Token 附加、401 自动刷新、错误上报 3. 后端统一返回 `{code, message, data}`,`ApiClient._request()` 自动解析 4. 异常类型:`ServerException`(code != 0)、`NetworkException`(连接失败) 5. **不在 Widget/Controller 中直接调用 ApiClient**,应通过 Repository 层 ### 错误处理链 ``` DataSource (throw Exception) → Repository (catch → Either) → Controller (fold → AsyncValue) → UI (AsyncValue.when: loading/data/error) ``` --- ## 四、路由(go_router) ### 规则 1. 所有路由定义在 `core/router/app_router.dart` 2. 使用 `context.go()` 进行声明式导航,避免 `Navigator.push()`(页面内二级跳转除外) 3. 登录态检查在 `redirect` 中统一处理 4. 产品类型 → 路由映射在 `_productRoutes` Map 中维护 --- ## 五、性能规范 ### 内存管理 1. **所有 Stream 订阅必须存入变量,dispose 时取消** ```dart // ✅ 正确 StreamSubscription? _positionSub; _positionSub = stream.listen((data) { ... }); @override void dispose() { _positionSub?.cancel(); super.dispose(); } // ❌ 错误 — 无法取消,造成内存泄漏 stream.listen((data) { ... }); ``` 2. **AnimationController、VideoPlayerController、AudioPlayer 等必须在 dispose 中释放** 3. **页面离开时停止 BLE 扫描** ```dart @override void dispose() { FlutterBluePlus.stopScan(); _scanSubscription?.cancel(); super.dispose(); } ``` ### Widget 重建优化 4. **能用 `const` 的地方必须加 `const`** ```dart // ✅ const SizedBox(height: 16) const EdgeInsets.all(16) const Text('Hello') // ❌ SizedBox(height: 16) EdgeInsets.all(16) ``` 5. **动画区域用 `RepaintBoundary` 包裹**,防止全页面重绘 ```dart RepaintBoundary( child: AnimatedGradientBackground(), ) ``` 6. **`AnimatedBuilder` 必须使用 `child` 参数**,将不变的子树传入 child,不要在 builder 中重建 ```dart // ✅ AnimatedBuilder( animation: _controller, child: const HeavyWidget(), // 只构建一次 builder: (context, child) => Transform.rotate( angle: _controller.value, child: child, // 复用 ), ) ``` ### API 调用 7. **多个独立 API 调用使用 `Future.wait` 并行**,不要顺序 await ```dart // ✅ 并行 final results = await Future.wait([ api.get('/shelves/'), api.get('/stories/'), ]); // ❌ 顺序(慢 N 倍) final shelves = await api.get('/shelves/'); final stories = await api.get('/stories/'); ``` 8. **耗时计算(JSON 解析、图片处理)放到 Isolate** ```dart final result = await Isolate.run(() => jsonDecode(bigJson)); ``` ### 图片 9. **网络图片使用 `Image.network` + `fit: BoxFit.cover`**,不要手动指定 `cacheWidth/cacheHeight`(会导致显示异常) 10. **大量图片列表使用 `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 不会崩溃)