| Topic | Link |
|---|---|
| Architecture | #architecture |
| Data Flow | #data-flow |
| Currency Handling | #currency-handling |
| Image Processing | #image-processing |
| Error Handling | #error-handling |
| Testing | #testing |
| Performance | #performance-optimization |
| Troubleshooting | #troubleshooting |
┌─────────────────────────────────────────┐
│ Presentation │
│ (Screens, Widgets, Providers) │
├─────────────────────────────────────────┤
│ Domain │
│ (Repository Interfaces, Entities) │
├─────────────────────────────────────────┤
│ Data │
│ (Repositories, Models, DataSources) │
└─────────────────────────────────────────┘
| Layer | Key Files |
|---|---|
| Presentation | providers/*.dart, screens/*.dart |
| Domain | domain/repositories/*.dart |
| Data | data/repositories/*.dart, data/models/*.dart |
User Input
↓
AddExpenseScreen
↓
ExpenseProvider.addExpense()
↓
ExpenseRepository.createExpense()
↓
DatabaseHelper.insert()
↓
SQLite
User selects currency
↓
ExchangeRateProvider.fetchRate()
↓
ExchangeRateRepository.getExchangeRate()
↓
[Check cache] → [Cache valid?]
↓ No ↓ Yes
ExchangeRateApi Return cached
↓
Store in cache
↓
Return rate
User taps Export
↓
ExportScreen
↓
ExportService.exportMonthlyReport()
↓
[Generate Excel] + [Copy images to temp]
↓
Archive into ZIP
↓
share_plus.shareXFiles()
// 使用分儲存避免浮點誤差
final amountCents = (userInput * 100).round();
// 轉換回元顯示
final displayAmount = amountCents / 100;
// 匯率以 ×10⁶ 精度儲存
const ratePrecision = 1000000;
// 儲存
final storedRate = (apiRate * ratePrecision).round();
// 使用
final convertedCents = (originalCents * storedRate) ~/ ratePrecision;
| Code | Name | Symbol |
|---|---|---|
| HKD | Hong Kong Dollar | HK$ |
| CNY | Chinese Yuan | ¥ |
| USD | US Dollar | US$ |
enum ExchangeRateSource {
api, // 即時 API
cache, // 本地快取
fallback, // 預設匯率
manual, // 手動輸入
}
| Type | Max Size | Quality | Format |
|---|---|---|---|
| Original | 1920×1080 | 85% | JPEG |
| Thumbnail | 200px width | 70% | JPEG |
[App Private Directory]
└── receipts/
├── {uuid}.jpg # 原圖
└── {uuid}_thumb.jpg # 縮圖
// 成功
return Result.success(expense);
// 失敗
return Result.failure(AppException(
code: AppException.databaseError,
message: '儲存失敗',
originalError: e,
));
final result = await repository.createExpense(expense);
result.fold(
onFailure: (e) => showError(e.message),
onSuccess: (expense) => navigateToDetail(expense.id),
);
| Code | Description | 處理方式 |
|---|---|---|
| NETWORK_ERROR | 網路錯誤 | 使用快取/重試 |
| DATABASE_ERROR | 資料庫錯誤 | 顯示錯誤訊息 |
| VALIDATION_ERROR | 驗證失敗 | 顯示欄位錯誤 |
| AUTH_ERROR | 認證失敗 | 重新登入 |
| IMAGE_ERROR | 圖片處理失敗 | 跳過圖片 |
| Category | Path | Purpose |
|---|---|---|
| Unit | test/core/, test/data/ |
邏輯測試 |
| Widget | test/presentation/ |
UI 測試 |
| Accessibility | test/accessibility/ |
無障礙測試 |
# 生成 mocks
dart run build_runner build --delete-conflicting-outputs
// 檔案命名
test/path/to/file_test.dart
// Mock 檔案
test/path/to/file_test.mocks.dart
// 測試結構
void main() {
group('ClassName', () {
late MockDependency mockDep;
setUp(() {
mockDep = MockDependency();
});
test('should do something', () {
// Arrange
when(mockDep.method()).thenReturn(value);
// Act
final result = sut.doSomething();
// Assert
expect(result, expectedValue);
});
});
}
// ❌ 避免:整個 widget 隨任何變更 rebuild
Consumer<ExpenseProvider>(
builder: (_, provider, __) => Text(provider.total),
)
// ✅ 建議:只監聽需要的屬性
Selector<ExpenseProvider, String>(
selector: (_, p) => p.formattedTotal,
builder: (_, total, __) => Text(total),
)
// 列表項目加入 RepaintBoundary 避免整個列表重繪
RepaintBoundary(
child: ExpenseCard(expense: expense),
)
// 圖片縮圖快取 (lib/core/utils/lru_cache.dart)
final thumbnailCache = LruCache<String, Uint8List>(
maxSize: 50, // 最多 50 張縮圖
);
| 指標 | 目標 | 實際 | |——|——|——| | 冷啟動 | < 2s | ✅ | | 列表滾動 | 60fps | ✅ | | 記憶體洩漏 | 0 | ✅ |
原因:網路錯誤且無快取 解決:
exchange_rate_cache 表原因:路徑不存在或權限問題 解決:
receiptImagePath 是否正確PathValidator 結果原因:Token 過期或權限不足 解決:
flutter_secure_storage 運作正常原因:未生成 mocks 解決:
dart run build_runner build --delete-conflicting-outputs
原因:資料庫初始化或清理任務 解決:
_initializeApp() 日誌_performStartupCleanup() 執行時間lib/presentation/providers/xxx_provider.dartChangeNotifiermain.dart 的 MultiProvider 中註冊test/presentation/providers/xxx_provider_test.dartlib/presentation/screens/xxx/xxx_screen.dartapp_router.dart 新增路由PageRoute 類型domain/repositories/ 介面新增方法data/repositories/ 實作Result<T> 型別DatabaseHelper._databaseVersion_onUpgrade 處理 migrationfromMap/toMap