Skip to content

Latest commit

 

History

History
693 lines (525 loc) · 17.1 KB

File metadata and controls

693 lines (525 loc) · 17.1 KB

开发者指南

本文档面向 CalcLab 二次开发者,详细介绍项目架构、模块划分、扩展方法与最佳实践。


目录


1. 架构总览

CalcLab 采用经典的三层架构:

┌─────────────────────────────────────────────┐
│           UI 层 (lib/ui/)                   │
│  - Flutter Widget                            │
│  - 用户交互                                  │
│  - 页面路由                                  │
└──────────────┬──────────────────────────────┘
               │ 调用
               ▼
┌─────────────────────────────────────────────┐
│         业务核心层 (lib/core/)              │
│  - 纯 Dart 实现(不依赖 Flutter)           │
│  - 可独立抽取为 Dart 包                     │
│  - 易于单元测试                              │
└──────────────┬──────────────────────────────┘
               │ 使用
               ▼
┌─────────────────────────────────────────────┐
│       基础设施层(Flutter 平台)            │
│  - sqlite、path_provider、shared_preferences│
│  - pdf、image、fl_chart                     │
│  - flutter_math_fork                        │
└─────────────────────────────────────────────┘

1.1 分层原则

  • UI 层不包含业务逻辑,仅负责渲染和事件分发
  • 业务核心层不依赖 Flutter Widget,可在 Dart VM、Node.js、Flutter Web 中复用
  • 基础设施层通过 lib/core/storage/lib/core/export/ 等适配器模式解耦

1.2 依赖方向

ui → core → infrastructure

绝对禁止反向依赖。


2. 模块设计

2.1 业务核心(lib/core/)

calculator/

文件 职责
expression_engine.dart 表达式求值入口,封装 math_expressions
complex.dart 复数类与运算
matrix.dart 矩阵类与运算
equation_solver.dart 一元/多元方程求解
latex_to_expression.dart LaTeX → 表达式(反向解析)
math_renderer.dart LaTeX → Flutter Widget(封装层)

statistics/

  • 8 个独立模块,按功能拆分
  • 每个模块导出静态方法,无状态
  • 输入:原始数据 List<double>
  • 输出:标量 / Map / 统计对象

units/

  • unit_converter.dart:单位换算核心,支持动态注册新单位
  • physical_constants.dart:常量数据库(Map<String, ConstantEntry>

ai/(P2)

  • formula_recognizer.dart:抽象接口 + 本地实现
  • local_rule_engine.dart:基于规则的本地方案(默认)
  • nl_query_engine.dart:自然语言意图识别
  • ai_service.dart:AI 服务总入口(预留远程接入)

share/(P2)

  • share_service.dart:分享码编解码

storage/

  • history_model.dart:历史数据模型(不可变)
  • history_store.dart:SQLite 持久化(CRUD)

export/

  • csv_exporter.dart:数据 → CSV
  • image_exporter.dart:Widget → PNG(RepaintBoundary)

report/

  • report_generator.dart:报告生成(PDF + Markdown)

2.2 UI 层(lib/ui/)

每个页面单独成目录,结构:

ui/<feature>/
├── <feature>_page.dart       # 主页面 Widget
└── widgets/                  # 子组件(可选)
    └── xxx.dart

主入口 main.dart 通过 IndexedStack 维护 4 个主 Tab:

  • CalculatorPage:主计算器
  • DataPage:数据清洗
  • ChartPage:图表
  • HistoryPage:历史

高级功能通过主计算器右上角菜单的二级页面进入。


3. 状态管理

CalcLab 暂未引入第三方状态管理库,遵循以下原则:

3.1 本地状态

  • StatefulWidget + setState 管理页面级状态
  • 不滥用全局变量

3.2 跨页面状态

  • 通过 InheritedWidget 暴露(如主题)
  • 通过构造函数传递(如共享的数据对象)
  • 历史数据通过 HistoryStore 单例访问

3.3 历史存储

// 写入
await HistoryStore.instance.add(
  HistoryEntry(
    expression: '1+2',
    result: '3',
    timestamp: DateTime.now(),
  ),
);

// 读取
final all = await HistoryStore.instance.all();

// 监听变化
HistoryStore.instance.changes.listen((event) {
  // 刷新 UI
});

4. 扩展指南

4.1 新增计算函数

假设新增 cbrt(x)(立方根)函数:

步骤 1:在 expression_engine.dart 中注册

class ExpressionEngine {
  static double evaluate(String expression) {
    // 包装 math_expressions
    final parser = Parser();
    final ctx = ContextModel();
    
    // 注册自定义函数
    ctx.bindVariableName('cbrt', (num x) => _cbrt(x.toDouble()));
    
    return parser.parse(expression).evaluate(EvaluationType.REAL, ctx);
  }
  
  static double _cbrt(double x) => x.isNegative 
    ? -((-x) ** (1/3)) 
    : x ** (1/3);
}

步骤 2:在 UI 键盘添加按钮

编辑 lib/ui/calculator/keypad.dart,在合适位置添加:

CalcButton(
  label: '∛',
  onTap: () => _onFunction('cbrt('),
),

步骤 3:添加测试

// test/expression_engine_test.dart
test('cbrt(27) = 3', () {
  expect(ExpressionEngine.evaluate('cbrt(27)'), closeTo(3.0, 1e-9));
});

步骤 4:更新文档

  • 更新 USER_GUIDE.md 的「支持的运算」表格
  • 更新 README.md 的特性列表

4.2 新增统计方法

假设新增 geometricMean(List<double> data)

步骤 1:创建 lib/core/statistics/central_tendency.dart

class CentralTendency {
  /// 几何平均:所有值乘积的 n 次方根
  /// 注意:数据必须为正
  static double geometricMean(List<double> data) {
    if (data.isEmpty) throw ArgumentError('Data cannot be empty');
    if (data.any((x) => x <= 0)) {
      throw ArgumentError('Geometric mean requires all positive values');
    }
    final product = data.reduce((a, b) => a * b);
    return math.pow(product, 1 / data.length).toDouble();
  }
}

步骤 2:导出

编辑 lib/core/statistics/ 的统一导出文件(如果存在),或直接 import 使用。

步骤 3:添加测试

// test/statistics_test.dart
test('geometricMean of [1, 2, 4, 8] = 2.828...', () {
  expect(
    CentralTendency.geometricMean([1, 2, 4, 8]),
    closeTo(2.8284271247461903, 1e-9),
  );
});

步骤 4:UI 集成(如需要)

lib/ui/data/data_page.dart 中添加到统计菜单。

4.3 接入新 AI 服务

CalcLab 采用"接口预留 + 本地兜底"模式,新增 AI 服务步骤:

步骤 1:实现 FormulaRecognizer 接口

// lib/core/ai/remote_formula_recognizer.dart
import 'package:http/http.dart' as http;
import 'dart:convert';

class RemoteFormulaRecognizer implements FormulaRecognizer {
  final String apiKey;
  final String endpoint;
  
  RemoteFormulaRecognizer({
    required this.apiKey,
    required this.endpoint,
  });
  
  @override
  Future<FormulaRecognitionResult> recognize(String latex) async {
    final response = await http.post(
      Uri.parse(endpoint),
      headers: {
        'Authorization': 'Bearer $apiKey',
        'Content-Type': 'application/json',
      },
      body: jsonEncode({'latex': latex}),
    );
    
    if (response.statusCode != 200) {
      throw Exception('AI service error: ${response.statusCode}');
    }
    
    final data = jsonDecode(response.body);
    return FormulaRecognitionResult(
      latex: latex,
      expression: data['expression'],
      confidence: data['confidence'] ?? 0.0,
      explanation: data['explanation'],
    );
  }
}

步骤 2:在工厂方法中按需选择

// lib/core/ai/formula_recognizer.dart
FormulaRecognizer createDefaultRecognizer({String? apiKey}) {
  if (apiKey != null && apiKey.isNotEmpty) {
    return RemoteFormulaRecognizer(
      apiKey: apiKey,
      endpoint: 'https://api.example.com/recognize',
    );
  }
  return LocalFormulaRecognizer();
}

步骤 3:UI 暴露配置入口

在设置页面添加 API Key 输入框。

4.4 新增单位类别

编辑 lib/core/units/unit_converter.dart

class UnitCategory {
  final String name;          // 物理量名
  final String symbol;        // 量符号
  final List<UnitEntry> units;
}

class UnitEntry {
  final String name;          // 单位名
  final String symbol;        // 缩写
  final double factor;        // 相对 SI 的换算系数
  final double? offset;       // 偏移(如温度)
}

// 添加新类别
static final UnitCategory dataStorage = UnitCategory(
  name: '数据存储',
  symbol: 'S',
  units: [
    UnitEntry(name: '字节',  symbol: 'B',   factor: 1),
    UnitEntry(name: '千字节', symbol: 'KB',  factor: 1024),
    UnitEntry(name: '兆字节', symbol: 'MB',  factor: 1024 * 1024),
    UnitEntry(name: '吉字节', symbol: 'GB',  factor: 1024 * 1024 * 1024),
    UnitEntry(name: '太字节', symbol: 'TB',  factor: 1024 * 1024 * 1024 * 1024),
  ],
);

并在 UnitConverter.categories 中注册。

4.5 自定义 UI 页面

步骤 1:创建页面文件

// lib/ui/custom/my_feature_page.dart
import 'package:flutter/material.dart';

class MyFeaturePage extends StatefulWidget {
  const MyFeaturePage({super.key});
  
  @override
  State<MyFeaturePage> createState() => _MyFeaturePageState();
}

class _MyFeaturePageState extends State<MyFeaturePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('我的功能')),
      body: const Center(child: Text('Hello CalcLab')),
    );
  }
}

步骤 2:添加导航入口

例如添加到主计算器菜单:

// lib/ui/calculator/calculator_page.dart
PopupMenuButton<String>(
  itemBuilder: (context) => [
    const PopupMenuItem(value: 'my_feature', child: Text('我的功能')),
    // ... 其他项
  ],
  onSelected: (value) {
    if (value == 'my_feature') {
      Navigator.push(
        context,
        MaterialPageRoute(builder: (_) => const MyFeaturePage()),
      );
    }
  },
)

步骤 3:添加 Widget 测试

// test/widget_test.dart
testWidgets('MyFeaturePage renders title', (tester) async {
  await tester.pumpWidget(
    const MaterialApp(home: MyFeaturePage()),
  );
  expect(find.text('我的功能'), findsOneWidget);
});

5. 测试规范

5.1 测试类型

类型 位置 框架 用途
单元测试 test/ flutter test 验证业务逻辑
Widget 测试 test/ flutter test 验证 UI 组件
集成测试 integration_test/ flutter test integration_test/ 端到端流程

5.2 命名规范

  • 文件:xxx_test.dart
  • 测试用例:使用描述性英文,should ... when ... 格式
test('should return geometric mean when given positive numbers', () {
  // ...
});

5.3 覆盖率要求

  • 业务核心层:≥ 90%
  • UI 层:≥ 60%
  • 总覆盖率:≥ 75%

查看覆盖率:

flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
# 打开 coverage/html/index.html

5.4 必测场景

  • 边界值(0、负数、空列表、极大/极小值)
  • 异常输入(null、非法字符)
  • 浮点精度(用 closeTo(expected, tolerance)
  • 并发安全(历史存储、分享码生成)

6. 代码风格

6.1 格式化

# 格式化全部
dart format .

# 检查未格式化
dart format --output=none --set-exit-if-changed .

6.2 Lint

analysis_options.yaml 启用 flutter_lints,提交前必须:

flutter analyze

6.3 命名规范

类型 规范 示例
PascalCase HistoryStore
方法/函数 camelCase evaluate()
变量 camelCase result
常量 lowerCamelCase 或 SCREAMING pi / MAX_SIZE
私有成员 前缀下划线 _internalState
文件 snake_case history_store.dart

6.4 注释

  • 公共 API 必须有 dartdoc 注释
  • 中文注释仅用于业务逻辑说明
  • 关键算法附上原理链接或公式
/// 计算皮尔逊相关系数
/// 
/// 公式:r = Σ((xi - x̄)(yi - ȳ)) / sqrt(Σ(xi - x̄)² × Σ(yi - ȳ)²)
/// 
/// 范围:[-1, 1],1 表示完全正相关,-1 表示完全负相关
double pearsonCorrelation(List<double> x, List<double> y) {
  // ...
}

7. 提交规范

采用 Conventional Commits

7.1 格式

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

<body>

<footer>

7.2 Type

Type 用途
feat 新功能
fix 修复 bug
docs 仅文档
style 格式(不影响代码运行)
refactor 重构
perf 性能优化
test 测试相关
chore 构建/工具/依赖

7.3 示例

git commit -m "feat(calculator): add cbrt() function"
git commit -m "fix(storage): handle concurrent history writes"
git commit -m "docs: update USER_GUIDE with new features"

7.4 分支策略

  • main:稳定主分支
  • feature/*:特性开发
  • fix/*:bug 修复
  • release/*:发布准备
  • hotfix/*:紧急修复

工作流:feature → main(PR + review)


8. 调试技巧

8.1 日志

import 'package:flutter/foundation.dart';

// 仅 debug 模式打印
debugPrint('History added: $entry');

// 性能追踪
final timeline = Stopwatch()..start();
// ... 代码
timeline.stop();
debugPrint('Operation took ${timeline.elapsedMilliseconds}ms');

8.2 DevTools

# 启动 DevTools
flutter run --debug
# 在输出中找到 DevTools URL 并打开

主要功能:

  • Inspector:查看 Widget 树
  • Memory:内存泄漏检测
  • Performance:帧时间分析
  • Network:HTTP 请求追踪

8.3 断点调试

VS Code:在代码行号左侧点击 → 设置断点 → F5 启动调试

8.4 单元测试调试

# 单个文件 + 输出
flutter test test/expression_engine_test.dart --reporter expanded

# 显示 print 输出
flutter test --reporter expanded --concurrency=1

8.5 常见问题排查

Gradle 构建慢

编辑 android/gradle.properties

org.gradle.jvmargs=-Xmx4G
org.gradle.parallel=true
org.gradle.caching=true

Pub get 慢

使用国内镜像:

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
flutter pub get

iOS Pod install 慢

cd ios
pod install --repo-update

附录 A:核心算法参考

A.1 LaTeX 反向解析

位置:lib/core/calculator/latex_to_expression.dart

支持命令:

  • \frac{a}{b}a/b
  • \sqrt{x}sqrt(x)
  • \pipi
  • x^{n}x^n
  • \times\cdot*
  • \left( \right)( )
  • \sum_{i=1}^{n}sum(i=1,n,...)

A.2 自然语言意图识别

位置:lib/core/ai/nl_query_engine.dart

三层识别:

  1. 关键词 + 正则 匹配统计请求
  2. 运算符检测 识别为表达式求值
  3. 阶乘模式 提取阶乘运算

A.3 分享码编解码

位置:lib/core/share/share_service.dart

  • 编码:UTF-8 JSON → base64,加 CALCLAB: 前缀
  • 解码:剥离前缀 → base64 decode → UTF-8 JSON

附录 B:常见扩展用例

场景 涉及文件 难度
新增三角函数 expression_engine.dartkeypad.dart
新增统计方法 新建 core/statistics/xxx.dart ⭐⭐
新增单位类别 unit_converter.dart
接入云端 AI core/ai/remote_xxx.dart ⭐⭐⭐
新增图表类型 fl_chart 文档参考 ⭐⭐
多语言支持 创建 lib/l10n/arb 文件 ⭐⭐
暗色主题 ThemeData.dark()

附录 C:参考资料


📝 最后更新:2026-07-08 · 基于 CalcLab v1.0.0