rtc_prd/airhub_app/CLAUDE.md
2026-03-26 11:12:02 +08:00

8.6 KiB
Raw Permalink Blame History

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>fpdartController 内 fold 处理
  4. 页面中优先使用 ConsumerWidget(无状态);需要动画/本地状态时用 ConsumerStatefulWidget
  5. ref.watch() 用于 UI 绑定(触发重建),ref.read() 用于事件处理(不触发重建)
  6. 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),
    );
  }
}

三、网络层

规则

  1. 所有 API 调用通过 ApiClientDio 封装),不直接使用 http 包或 Dio()
  2. ApiClient 自动处理Token 附加、401 自动刷新、错误上报
  3. 后端统一返回 {code, message, data}ApiClient._request() 自动解析
  4. 异常类型:ServerExceptioncode != 0NetworkException(连接失败)
  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 时取消

    // ✅ 正确
    StreamSubscription<Duration>? _positionSub;
    _positionSub = stream.listen((data) { ... });
    
    @override
    void dispose() {
      _positionSub?.cancel();
      super.dispose();
    }
    
    // ❌ 错误 — 无法取消,造成内存泄漏
    stream.listen((data) { ... });
    
  2. AnimationController、VideoPlayerController、AudioPlayer 等必须在 dispose 中释放

  3. 页面离开时停止 BLE 扫描

    @override
    void dispose() {
      FlutterBluePlus.stopScan();
      _scanSubscription?.cancel();
      super.dispose();
    }
    

Widget 重建优化

  1. 能用 const 的地方必须加 const

    // ✅
    const SizedBox(height: 16)
    const EdgeInsets.all(16)
    const Text('Hello')
    
    // ❌
    SizedBox(height: 16)
    EdgeInsets.all(16)
    
  2. 动画区域用 RepaintBoundary 包裹,防止全页面重绘

    RepaintBoundary(
      child: AnimatedGradientBackground(),
    )
    
  3. AnimatedBuilder 必须使用 child 参数,将不变的子树传入 child不要在 builder 中重建

    // ✅
    AnimatedBuilder(
      animation: _controller,
      child: const HeavyWidget(),  // 只构建一次
      builder: (context, child) => Transform.rotate(
        angle: _controller.value,
        child: child,  // 复用
      ),
    )
    

API 调用

  1. 多个独立 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/');
    
  2. 耗时计算JSON 解析、图片处理)放到 Isolate

    final result = await Isolate.run(() => jsonDecode(bigJson));
    

图片

  1. 网络图片使用 Image.network + fit: BoxFit.cover,不要手动指定 cacheWidth/cacheHeight(会导致显示异常)
  2. 大量图片列表使用 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 自动生成,不手动编辑

类命名

  • PascalCaseAuthControllerDeviceControlPageServerException
  • 私有前缀:_buildHeader()_positionSub

变量命名

  • camelCasecurrentUserisPlaying_audioPlayer
  • ProviderxxxProvider(由 @riverpod 自动生成)

八、主题系统

颜色

  • 全局颜色定义在 theme/app_colors.dart
  • 产品主题颜色在 theme/product_theme.dart4 种产品类型)
  • 不要在 Widget 中硬编码颜色值,优先使用 AppColors.xxxTheme.of(context)

字体

  • 主字体DM Sans正文、Outfit标题、Press Start 2P品牌像素风
  • 通过 google_fonts 包引用,运行时自动下载并缓存
  • 本地字体Interpubspec.yaml 中声明)

间距

  • 使用 AppSpacing.xs/sm/md/lg/xl 常量
  • 不要在 Widget 中硬编码间距数值

九、Git 规范

分支命名

  • 功能分支:fix/auto-YYYYMMDD-HHMMSS
  • 合并目标:main

Commit 规范

  • fix: — Bug 修复
  • feat: — 新功能
  • refactor: — 重构(不改变行为)
  • chore: — 构建配置、依赖更新等

十、检查清单PR 提交前)

  • dart analyze 无 ErrorWarning/Info 可接受)
  • 所有 Stream 订阅已在 dispose 中取消
  • 所有 Controller/Player 已在 dispose 中释放
  • 多个独立 API 调用已用 Future.wait 并行化
  • 动画 Widget 使用了 RepaintBoundaryAnimatedBuilder(child:)
  • 新增的 Widget 构造函数尽可能标记为 const
  • 列表使用 ListView.builder,而非 ListView(children:)
  • 未引入新的硬编码颜色值(使用 AppColors 或 theme
  • Android release 构建正常minify + shrinkResources 不会崩溃)