295 lines
8.6 KiB
Markdown
295 lines
8.6 KiB
Markdown
# 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<T>` 管理异步状态
|
||
3. Repository 返回 `Either<Failure, T>`(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<MyState> 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<Failure, T>)
|
||
→ 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<Duration>? _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 不会崩溃)
|