Skip to content

开发文档

本文档整合了代码规范和常见问题,为日常开发提供参考。


1. 代码规范

1.1 文件命名规范

Dart 文件:小写蛇形命名法

✅ 正确:ocr_repository.dart, vocabulary_notebook_page.dart
❌ 错误:OCRRepo.dart, vocab_notebook.dart

目录命名:小写蛇形命名法,复数形式表示集合

features/
├── photo_recognition/
├── vocabulary_filtering/
└── statistics/

1.2 命名规范

类名:大驼峰命名法

dart
✅ 正确:class VocabularyNotebookPage {}
❌ 错误:class vocabulary_notebook_page {}

变量和方法:小驼峰命名法

dart
✅ 正确:
String vocabularyName;
void addVocabulary() {}
String _privateField;  // 私有成员前缀下划线

❌ 错误:
String Vocabulary_Name;
void AddVocabulary() {}

常量:小驼峰或全大写蛇形

dart
const defaultRetryCount = 3;  // 推荐
const MAX_RETRY_COUNT = 3;    // 也可

枚举:enum 本体大驼峰,值小驼峰

dart
enum SourceType {
  book,
  article,
  other,
}

1.3 文件结构顺序

dart
// 1. 导入部分(分组)
import 'package:flutter/material.dart';          // Flutter 库
import 'package:riverpod/riverpod.dart';         // 第三方库
import 'package:reading_vocab_helper/core/...'; // 项目内部

// 2. 类定义
/// 类文档注释
class MyClass {
  // 3. 成员变量
  final String _privateField;

  // 4. 构造函数
  MyClass(this._privateField);

  // 5. 公共方法
  void publicMethod() {}

  // 6. 私有方法
  void _privateMethod() {}

  // 7. 内部类/枚举
  enum _InternalEnum {}
}

1.4 注释规范

文档注释:使用 ///

dart
/// 词汇实体类
///
/// 代表一个单词及其学习信息
class Vocabulary {
  /// 计算下次复习日期
  ///
  /// 使用 SM-2 算法计算
  /// [rating] 用户评分 (1-4)
  /// [currentInterval] 当前间隔天数
  /// 返回下次复习的间隔天数
  int calculateNextInterval({
    required int rating,
    required int currentInterval,
  }) {
    // 实现...
  }
}

行内注释:使用 // 解释复杂逻辑

dart
// SM-2 算法核心公式
// EF' = EF + (0.1 - (3 - q) * (0.08 + (3 - q) * 0.02))
final newEF = easinessFactor + (0.1 - (3 - rating) * (0.08 + (3 - rating) * 0.02));

1.5 格式规范

代码格式化

bash
dart format .                                      # 格式化所有代码
dart format --output=none --set-exit-if-changed . # 仅检查

行长度

  • 推荐:80 字符
  • 硬性限制:120 字符

缩进:2 空格(不使用 Tab)

1.6 Dart 最佳实践

使用 const 构造函数

dart
✅ 正确:const Card(child: Text('Hello'));
❌ 错误:Card(child: Text('Hello'));

使用 Null Safety

dart
String? nullableName;
String nonNullableName = 'Larry';

void printName(String? name) {
  print(name ?? 'Unknown');
}

使用扩展方法

dart
extension StringExtension on String {
  bool get isNotBlank => isNotEmpty;
}

// 使用
if (text.isNotBlank) { ... }

1.7 Riverpod 规范

Provider 命名

  • 后缀 Provider:vocabularyProvider
  • 后缀 Notifier:vocabularyNotifier

使用 riverpod_generator

dart
@riverpod
class VocabularyFiltering extends _$VocabularyFiltering {
  @override
  FutureOr<FilteringResultEntity> build() {
    return FilteringResultEntity.initial();
  }

  Future<void> filterVocabularies(List<String> words) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      final repository = ref.read(vocabularyFilteringRepositoryProvider);
      return await repository.filterVocabularies(words: words);
    });
  }
}

1.8 Git 提交规范

提交消息格式

<type>(<scope>): <subject>

<body>
<footer>

类型(type)

  • feat: 新功能
  • fix: Bug 修复
  • docs: 文档更新
  • style: 代码格式(不影响功能)
  • refactor: 重构
  • test: 测试相关
  • chore: 构建/工具链相关

示例

feat(ocr): 添加百度 OCR 支持

- 集成百度 OCR API
- 添加 API 配置管理
- 实现错误处理和重试

Closes #123

1.9 i18n / 国际化开发规范

状态:i18n 迁移已全部完成(1045 个 key,EN/ZH 双语;ja 为部分翻译,84 个)。以下规范适用于所有新功能开发。

⚠️ 存量债:2026-08-03 实测 1045 个 key 中有 295 个已无代码引用(28%),迁移完成时记录的「0 未用」不变量已失效。新增 key 不受影响,但全量清扫需独立立项(/i18n-check)。

核心原则

所有用户可见的 UI 文本必须使用 ARB key,禁止硬编码字符串。

dart
// ✅ 正确
final l10n = AppLocalizations.of(context);
Text(l10n.commonCancel)

// ❌ 错误
Text('Cancel')
Text('取消')

什么必须国际化

必须国际化不需要国际化
按钮文字、标签、标题代码注释(中英文均可)
提示信息、错误消息(用户可见的)AppLogger 日志消息
对话框文字Failure 类内部错误消息
SnackBar / Toast 文字Notifier/Provider 内部错误(无 BuildContext)
空状态提示、引导文字技术调试参数名(开发者工具)
Tooltip 文字

ARB Key 命名规范

格式模块前缀 + 描述(camelCase)

commonCancel          — 跨模块共享(3+ 模块使用)
notebookPageTitle     — 模块级
notebookDeleteConfirm — 具体场景

已有前缀

前缀模块示例
common跨模块共享commonCancel, commonSave
app应用级appTitle
books书籍管理booksAddBook
cefrCEFR 级别cefrA1Name
dev开发者工具devDebugSettings
excluded排除词库excludedAddWord
filter词汇过滤filterConfirmTitle
notebook词汇笔记本notebookPageTitle
photo拍照识别photoCapture
settings设置settingsLanguage
shared共享页面sharedAboutTitle
source阅读来源sourceListTitle
translation翻译translationLookup
vocabTest词汇测试vocabTestTitle
welcome欢迎页welcomeMe

后缀约定

  • Title — 页面/对话框标题
  • Label — 表单标签
  • Hint — 输入提示
  • Message — 提示/通知消息
  • Action — 按钮操作文字
  • Error — 错误消息

新增 ARB Key 流程

bash
# 1. 在 app_en.arb 中添加 key(按字母序插入)
"notebookNewFeature": "My new feature",

# 2. 在 app_zh.arb 中添加对应翻译
"notebookNewFeature": "我的新功能",

# 3. 生成代码
flutter gen-l10n

# 4. 在 Dart 代码中使用
final l10n = AppLocalizations.of(context);
Text(l10n.notebookNewFeature)

带参数的字符串(ICU Message Format)

json
// app_en.arb
"commonWordCount": "{count} words"

// app_zh.arb
"commonWordCount": "{count} 个单词"
dart
// 使用
l10n.commonWordCount(42)  // "42 words" / "42 个单词"

Domain 层适配器模式

Domain 层没有 BuildContext,不能直接调用 l10n。使用适配器类桥接:

dart
// lib/config/cefr_descriptions.dart
class CefrDescriptions {
  static String name(BuildContext context, CefrLevel level) {
    final l10n = AppLocalizations.of(context);
    switch (level) {
      case CefrLevel.a1: return l10n.cefrA1Name;
      // ...
    }
  }
}

已有的适配器类

  • CefrDescriptions — CEFR 级别名称和描述
  • FontSizeDescriptions — 字体大小标签和描述
  • OcrModeDescriptions — OCR 模式名称和描述
  • RecommendationDescriptions — 复习分组推荐提示文案(v58)

⚠️ v58 教训:GroupRecommendationService 曾直接返回拼好的英文串 ('Due: 15 words'),RecommendationCard 原样渲染 → 中文界面显示英文。 Domain 层要输出「带数值的文案」时,返回结构化语义(枚举 + 数值)而不是 拼好的字符串,否则 adapter 无法在不反解字符串的前提下本地化。

当前支持的语言

语言ARB 文件状态
Englishlib/l10n/app_en.arb✅ 模板语言
中文lib/l10n/app_zh.arb✅ 完成

添加新语言

只需 3 步,不需要改任何 Dart 代码:

bash
# 1. 复制模板创建新的 ARB 文件
cp lib/l10n/app_en.arb lib/l10n/app_ja.arb

# 2. 修改 @@locale 并翻译所有 value
# "@@locale": "ja"

# 3. 重新生成
flutter gen-l10n

Flutter 会自动发现新的 ARB 文件并生成对应的 app_localizations_ja.dart

常见错误

dart
// ❌ 硬编码文字
Text('No data')

// ❌ 拼接字符串
Text('Found $count words')

// ❌ const + l10n(l10n 不是编译时常量)
const Text(l10n.commonCancel)

// ✅ 使用 l10n key
Text(l10n.commonNoData)

// ✅ 使用 ICU 参数
Text(l10n.commonWordCount(count))

// ✅ 去掉 const
Text(l10n.commonCancel)

2. 常见问题

2.1 开发环境问题

Q1: Flutter SDK 找不到

错误信息

Flutter SDK not found

解决方案

bash
# 检查 Flutter 是否已安装
flutter --version

# 确保 Flutter 已添加到 PATH
export PATH="$PATH:/path/to/flutter/bin"

Q2: 依赖安装失败

错误信息

Could not resolve all packages

解决方案

bash
flutter clean
flutter pub cache repair
flutter pub get

# 如果还是失败
flutter upgrade

Q3: iOS 模拟器启动失败

解决方案

bash
# 列出可用模拟器
xcrun simctl list devices

# 启动指定模拟器
xcrun simctl boot "iPhone 15 Pro"

# 重启模拟器
sudo xcrun simctl shutdown all

2.2 构建问题

Q4: Android 构建失败

解决方案

bash
flutter clean
flutter pub upgrade
flutter build apk --release

Q5: iOS 构建失败(CocoaPods)

解决方案

bash
cd ios
rm -rf Pods Podfile.lock
pod install

# 如果还是失败
sudo gem install cocoapods
pod install --repo-update

2.3 运行时问题

Q6: 应用启动白屏/黑屏

问题描述:应用启动后长时间白屏或黑屏

解决方案

  1. 检查 main.dart 中的 runApp() 是否正确
  2. 检查是否有阻塞的初始化代码
  3. 使用 FutureBuilder 延迟加载
dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MyApp());
}

Q7: Hot Reload 不生效

解决方案

bash
# 1. Hot Restart(Shift+R)
# 2. 完全重启
flutter run

# 3. 清理后重新运行
flutter clean && flutter run

2.4 OCR 问题

Q8: OCR 识别准确率低

问题描述:识别结果不准确或为空

解决方案

  1. 图片质量:确保图片清晰、光线充足
  2. 图片预处理:添加灰度化、二值化、降噪
  3. 裁剪区域:只识别文字区域,避免干扰
  4. 多引擎尝试:如果一个引擎失败,尝试另一个

图片预处理示例

dart
Future<Uint8List> preprocessImage(String imagePath) async {
  final image = img.decodeImage(await File(imagePath).readAsBytes())!;

  // 灰度化
  final grayscale = img.grayscale(image);

  // 调整对比度
  final contrast = img.adjustColor(grayscale, contrast: 1.2);

  return Uint8List.fromList(img.encodePng(contrast));
}

Q9: 百度 OCR 调用失败

错误信息

Invalid API Key or Quota exceeded

解决方案

  1. 检查 .env 文件中的 API Key 是否正确
  2. 确认百度 OCR 配额是否用完
  3. 检查网络连接
  4. 使用备用 OCR 引擎(Google ML Kit)

2.5 数据库问题

Q10: SQLite 数据库锁定

错误信息

DatabaseException: database is locked

解决方案

dart
// ✅ 正确:使用单例
final database = await openDatabase(
  path,
  version: 1,
  singleInstance: true,  // 确保单例
);

// ❌ 错误:多次打开数据库
final db1 = await openDatabase(path);
final db2 = await openDatabase(path);  // 错误!

Q11: 数据库迁移失败

错误信息

Migration failed: table already exists

解决方案

dart
void _createVocabulariesTable(Database db) async {
  await db.execute('''
    CREATE TABLE IF NOT EXISTS vocabularies (
      id TEXT PRIMARY KEY,
      word TEXT NOT NULL UNIQUE,
      ...
    )
  ''');
}

2.6 状态管理问题

Q12: Riverpod Provider 未找到

错误信息

StateError: Could not find the Provider

解决方案

dart
void main() {
  runApp(
    ProviderScope(  // 必须包裹
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ✅ 正确
    final state = ref.watch(vocabularyProvider);

    // ❌ 错误:直接访问
    // final state = vocabularyProvider;
  }
}

Q13: 状态更新不触发 UI 刷新

问题描述:修改状态后 UI 没有更新

解决方案

dart
// ✅ 正确:重新赋值
class VocabularyNotifier extends StateNotifier<VocabularyState> {
  VocabularyNotifier() : super(VocabularyInitial());

  void loadVocabularies() {
    state = VocabularyLoading();  // 重新赋值
    // ...
    state = VocabularyLoaded(data);
  }
}

// ❌ 错误:修改内部
// state.list.add(item);  // 不会触发更新

2.7 OpenCV 问题

Q14: Mat 对象内存泄漏

问题描述:应用内存持续增长,最终崩溃

解决方案:使用 MatScope 自动管理

dart
// ✅ 正确:使用 MatScope
MatScope.run(() {
  final gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY);
  final edges = cv.Canny(gray, 50, 150);
  // ... 处理逻辑
  // 所有 Mat 对象在作用域结束时自动释放
});

// ❌ 错误:忘记释放
final gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY);
// ... 忘记调用 gray.dispose()

Q15: 边缘检测超时或崩溃

问题描述findContours 检测到过多轮廓,导致超时或崩溃

解决方案

dart
// 1. 图像预缩放
final maxDim = max(image.width, image.height);
if (maxDim > 1200) {
  final scale = 1200.0 / maxDim;
  cv.resize(image, image, Size.zero, fx: scale, fy: scale);
}

// 2. 高斯模糊降噪
cv.gaussianBlur(image, image, (5, 5), 1.5);

// 3. 限制轮廓数量
final sortedContours = contours.toList()
  ..sort((a, b) => cv.contourArea(b).compareTo(cv.contourArea(a)));
final topContours = sortedContours.take(50).toList();

2.8 性能问题

Q16: 应用启动慢

问题描述:冷启动时间 > 3 秒

解决方案

  1. 延迟初始化非关键组件
  2. 使用 Isolate 处理耗时操作
  3. 优化数据库查询(添加索引)
  4. 减少首页加载的数据量
dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 只初始化关键组件
  await initCriticalServices();

  runApp(MyApp());

  // 延迟初始化其他组件
  Future.microtask(() async {
    await initNonCriticalServices();
  });
}

Q17: 数据库查询慢

问题描述:查询耗时 > 500ms

解决方案

  1. 为高频查询字段建立索引
  2. 使用 LIMIT 限制结果数量
  3. 避免 SELECT *,只查询需要的字段
  4. 使用批量操作代替逐条操作
sql
-- 添加索引
CREATE INDEX idx_notebook_next_review ON notebook_entries(next_review_date);
CREATE INDEX idx_vocabulary_word ON vocabulary_items(word);

-- 优化查询
SELECT id, word, next_review_date
FROM notebook_entries
WHERE next_review_date <= date('now')
LIMIT 20;

Q18: 内存占用过高

问题描述:应用内存峰值 > 250MB

解决方案

  1. 及时释放图片资源
  2. 使用图片缓存(cached_network_image)
  3. 限制列表项数量(使用分页)
  4. 避免重复创建大对象
dart
// ✅ 正确:及时释放
final image = await decodeImageFromList(bytes);
try {
  // 使用 image
} finally {
  image.dispose();  // 确保释放
}

// ✅ 正确:使用缓存
CachedNetworkImage(
  imageUrl: url,
  memCacheWidth: 200,  // 限制缓存大小
)

2.9 测试问题

Q19: 单元测试失败

问题描述:测试运行失败

解决方案

bash
# 运行所有测试
flutter test

# 运行单个测试文件
flutter test test/domain/entities/vocabulary_test.dart

# 查看详细输出
flutter test --verbose

# 更新 golden 文件
flutter test --update-goldens

Q20: Widget 测试崩溃

问题描述:Widget 测试时找不到 Provider

解决方案

dart
testWidgets('should display vocabulary list', (tester) async {
  await tester.pumpWidget(
    ProviderScope(  // 必须包裹 ProviderScope
      child: MaterialApp(
        home: VocabularyListPage(),
      ),
    ),
  );

  await tester.pump();

  expect(find.text('Hello'), findsOneWidget);
});

最后更新:2025-12-28 更新频率:遇到新问题时添加 文档状态:v1.0