1. 概述
R&D 模組實作產品生命週期管理(PLM),專注於光阻劑(Photoresist)配方研發。它是 App 八大營業循環架構中的研發循環。
| 項目 |
說明 |
| 產業 |
光阻劑(Photoresist)配方 R&D |
| 伺服器外掛 |
tw.topgiga.rnd(iDempiere OSGI plugin) |
| Flutter 用戶端 |
lib/features/rnd/ |
| 設計參考 |
Aras Innovator PLM 架構 |
1.1 模組範圍
| Phase |
功能 |
說明 |
| Phase 1 |
配方研發 |
配方 CRUD、PLM 版次追蹤、批次票、固含量計算、秤量配料 |
| Phase 2 |
實驗與測試 |
實驗計劃、測試執行、結果記錄、趨勢分析 |
| Phase 3a |
知識庫 |
可搜尋的文章庫、標籤分類、配方比對 |
| Phase 3b |
AI 推薦 |
基於目標特性的 AI 輔助配方建議 |
| Phase 4a |
實驗室管理 |
設備清冊(設定驅動)、校準追蹤 |
| Phase 4b |
派工管理 |
工單、研究人員指派、工作量儀表板 |
| Phase 4c |
績效管理 |
KPI 定義、雷達圖、團隊排行榜 |
| Phase 4d |
專案成本報表 |
材料 + 人工成本分析、預算 vs 實際、圓餅圖 |
2. 架構
2.1 高層架構
┌──────────────────────┐ HTTPS/JWT ┌─────────────────────────┐
│ Flutter App │ <──────────────────► │ iDempiere ERP │
│ lib/features/rnd/ │ idempiere-rest API │ + tw.topgiga.rnd │
│ │ /api/v1/models/... │ OSGI plugin │
│ + Claude AI API │ └─────────────────────────┘
│ (Recommendations) │
└──────────────────────┘
2.2 目錄結構
lib/features/rnd/
├── core/ # 共用 PLM 基礎設施
│ ├── formula_calc_service.dart # 用戶端固含量計算器
│ ├── material_type.dart # MaterialType enum
│ ├── plm_repository.dart # PLM 版次 & 生命週期 API
│ └── widgets/
│ ├── lifecycle_badge.dart # DR/IR/RL/OB 彩色標籤
│ ├── revision_selector.dart # 版次下拉選單
│ ├── revision_timeline.dart # 視覺化版次歷史
│ ├── responsive_layout.dart # 手機/平板斷點
│ └── solid_content_gauge.dart # SC% 環形儀表
│
├── project/
│ └── rnd_project_config.dart # 設定驅動 DocumentConfig
│
├── formula/
│ ├── data/formula_repository.dart # 配方 CRUD + 批次票 + 流程
│ ├── domain/
│ │ ├── formula_list_notifier.dart # 列表 + 生命週期篩選 + 分頁
│ │ ├── formula_detail_notifier.dart # 單一配方 + 明細狀態
│ │ └── formula_editor_notifier.dart # 編輯模式 + dirty 追蹤
│ └── presentation/
│ ├── formula_list_screen.dart
│ ├── formula_detail_screen.dart
│ ├── formula_editor_screen.dart
│ ├── formula_compare_screen.dart
│ └── widgets/ # 配方行磚、編輯器、摘要卡 等
│
├── dispensing/
│ ├── dispensing_screen.dart # 響應式:tabs(手機) / master-detail(平板)
│ └── dispensing_notifier.dart # 配方選擇、批次票產生
│
├── experiment/
│ ├── data/experiment_repository.dart
│ ├── domain/
│ │ ├── experiment_list_notifier.dart
│ │ └── experiment_detail_notifier.dart
│ └── presentation/
│ ├── experiment_list_screen.dart
│ ├── experiment_detail_screen.dart
│ ├── test_result_entry_screen.dart
│ └── test_trend_screen.dart # fl_chart 折線圖
│
├── knowledge_base/
│ ├── data/kb_repository.dart
│ ├── domain/kb_search_notifier.dart
│ └── presentation/
│ ├── kb_search_screen.dart # 全文搜尋、類型 & 標籤篩選
│ ├── article_viewer_screen.dart # 閱讀文章 + 標籤
│ ├── article_editor_screen.dart # Markdown 編輯器 + 預覽
│ └── formula_comparison_screen.dart # 並排配方差異比對
│
├── recommendation/
│ ├── data/recommendation_repository.dart
│ ├── domain/ai_recommendation_service.dart # Claude API 整合
│ └── presentation/
│ └── recommendation_wizard_screen.dart # 步驟式精靈
│
├── lab/
│ ├── equipment_config.dart # 設定驅動 DocumentConfig
│ ├── calibration_repository.dart
│ └── calibration_screen.dart # 校準歷史 + 警示
│
├── dispatch/
│ ├── data/dispatch_repository.dart
│ ├── domain/dispatch_notifier.dart
│ └── presentation/
│ ├── work_order_list_screen.dart
│ ├── work_order_detail_screen.dart # 詳情 + 指派 tabs
│ ├── my_assignments_screen.dart
│ └── workload_dashboard_screen.dart # 堆疊長條圖 (fl_chart)
│
├── performance/
│ ├── data/performance_repository.dart
│ └── presentation/
│ └── performance_dashboard_screen.dart # 雷達圖 + KPI 卡片 + 排行榜
│
├── cost/
│ ├── data/cost_repository.dart
│ └── presentation/
│ └── cost_dashboard_screen.dart # 圓餅圖 + 預算 vs 實際
│
└── rnd_routes.dart # 所有 GoRouter 路由定義
2.3 狀態管理模式
所有功能遵循 Riverpod 模式:
UI Screen (ConsumerWidget)
↓ ref.watch(provider)
StateNotifier / AsyncNotifier
↓ calls
Repository
↓ calls
ApiClient (Dio)
↓ HTTPS
iDempiere REST API
主要 Riverpod Providers:
| Provider |
Type |
用途 |
formulaRepositoryProvider |
Provider<FormulaRepository> |
配方 CRUD |
experimentRepositoryProvider |
Provider<ExperimentRepository> |
實驗 CRUD |
kbRepositoryProvider |
Provider<KBRepository> |
知識庫 |
recommendationRepositoryProvider |
Provider<RecommendationRepository> |
AI 推薦 |
calibrationRepositoryProvider |
Provider<CalibrationRepository> |
設備校準 |
dispatchRepositoryProvider |
Provider<DispatchRepository> |
工單 + 指派 |
performanceRepositoryProvider |
Provider<PerformanceRepository> |
KPI + 績效 |
costRepositoryProvider |
Provider<CostRepository> |
專案成本資料 |
plmRepositoryProvider |
Provider<PlmRepository> |
PLM 版次 & 生命週期 |
aiRecommendationServiceProvider |
Provider<AIRecommendationService> |
Claude API 呼叫 |
2.4 設定驅動 vs 自訂畫面
兩個功能使用 DocumentConfig 框架(通用列表/詳情畫面):
- RND Project(
rnd_project_config.dart)— AD_Window_ID: 1000904
- RND Equipment(
equipment_config.dart)— AD_Window_ID: 1000920
其餘功能都有自訂畫面,具備專門的 UI(編輯器、圖表、精靈)。
3. 伺服器端資料模型
3.1 iDempiere 資料表
PLM 核心表(跨所有 Phase 共用)
| Table |
用途 |
PLM_LifecycleDefinition |
定義生命週期類型(例如「配方生命週期」) |
PLM_LifecycleState |
生命週期中的狀態:DR(草稿)、IR(審核中)、RL(已發佈)、OB(已廢止) |
PLM_StateTransition |
允許的狀態轉換,可選觸發工作流 |
PLM_Revision |
任意表的版次記錄(AD_Table_ID + Record_ID) |
PLM_ChangeOrder |
工程變更命令(ECO),追蹤修改 |
Phase 1:配方研發
| Table |
用途 |
主要欄位 |
RND_Project |
研發專案容器 |
Name, DocStatus, M_PartType_ID, C_BPartner_ID |
RND_Formula |
配方表頭 |
Name, PLM_Revision_ID, LifecycleState, IsLatestRevision, SC_TotalPct |
RND_FormulaLine |
配方材料行 |
M_ProductSolidContent_ID, QtyEntered, SC_Weight, MaterialType |
M_ProductSolidContent |
含固含量的材料主檔 |
Name, SolidContent, MaterialType |
RND_BatchTicket |
生產批次票 |
RND_Project_ID, Scale |
RND_BatchTicketLine |
批次票材料行 |
M_ProductSolidContent_ID, QtyEntered |
Phase 2:實驗與測試
| Table |
用途 |
主要欄位 |
RND_Experiment |
實驗表頭 |
RND_Project_ID, RND_Formula_ID, Result, AD_User_ID |
RND_ExperimentLine |
實驗步驟 |
Line, Description, Result |
RND_TestSpec |
測試規格定義 |
Name, Method, Unit, Min/Max values |
RND_TestResult |
測試結果表頭 |
RND_Experiment_ID, RND_Formula_ID, DateTested |
RND_TestResultLine |
個別測試量測值 |
RND_TestSpec_ID, ResultValue, IsPass |
Phase 3:知識庫與推薦
| Table |
用途 |
主要欄位 |
RND_KBArticle |
知識庫文章 |
Name, Content (markdown), ArticleType, IsPublished |
RND_KBTag |
分類標籤 |
Name, TagGroup, Color |
RND_KBArticle_Tag |
文章-標籤多對多關聯 |
RND_KBArticle_ID, RND_KBTag_ID |
RND_Recommendation |
AI 推薦記錄 |
Target properties, AIModel, AIResponse, Confidence |
RND_RecommendationLine |
推薦材料行 |
M_ProductSolidContent_ID, SuggestedQty, Reasoning |
Phase 4:實驗室、派工、績效、成本
| Table |
用途 |
主要欄位 |
RND_Equipment |
實驗室設備清冊 |
Name, ModelNo, Status, NextCalibrationDate |
RND_Calibration |
校準記錄 |
RND_Equipment_ID, DateCalibrated, Result, CertificateNo |
RND_WorkOrder |
工單表頭 |
RND_Project_ID, DocStatus (DR/IP/CO), Priority |
RND_Assignment |
工作指派 |
RND_WorkOrder_ID, AD_User_ID, Role, Status, Hours |
RND_KPI |
KPI 定義 |
Name, Weight, TargetValue |
RND_Performance |
績效記錄 |
AD_User_ID, RND_KPI_ID, PeriodType, Score, ActualValue |
C_ProjectLine |
專案預算行(標準 iDempiere) |
PlannedAmt |
4. API 合約
4.1 REST 端點(iDempiere REST 外掛)
所有資料存取都透過標準 iDempiere REST API:
GET /api/v1/models/{tableName} # 列表查詢,支援 OData $filter, $orderby, $top, $skip, $expand
GET /api/v1/models/{tableName}/{id} # 取得單筆記錄
POST /api/v1/models/{tableName} # 建立記錄
PUT /api/v1/models/{tableName}/{id} # 更新記錄
DELETE /api/v1/models/{tableName}/{id} # 刪除記錄
4.2 伺服器流程(Server Processes)
| Process |
Endpoint |
Parameters |
用途 |
| GenerateBatchTicket |
POST /api/v1/processes/generatebatchticket |
RND_Formula_ID[], Scale |
從配方建立批次票 |
| CopyFromExistingFormula |
POST /api/v1/processes/copyfromexistingformula |
RND_Formula_ID, Record_ID |
複製配方到其他專案 |
| PLMCreateRevision |
POST /api/v1/processes/plmcreaterevision |
AD_Table_ID, Record_ID, Description |
建立新 PLM 版次 |
| PLMTransitionState |
POST /api/v1/processes/plmtransitionstate |
PLM_Revision_ID, Next_LifecycleState_ID |
轉換生命週期狀態 |
| EvaluateTestResult |
POST /api/v1/processes/evaluatetestresult |
RND_TestResult_ID |
伺服器端測試評估 |
4.3 外部 AI API
AI 推薦功能從 Flutter 用戶端直接呼叫 Claude API:
POST https://api.anthropic.com/v1/messages
Headers: x-api-key, anthropic-version: 2023-06-01
Model: claude-sonnet-4-5-20250929
Prompt 包含:
- 目標特性(SC%、黏度、膜厚、應用、基板)
- 來自
M_ProductSolidContent 的可用材料
- 相似的現有配方(在容差範圍內透過 SC% 匹配)
回應解析為 JSON:{materials: [...], explanation: "...", confidence: N}
5. 配方計算引擎
5.1 用戶端計算器(FormulaCalcService)
位於 lib/features/rnd/core/formula_calc_service.dart。
映射伺服器端 BMCalc.java 以提供即時預覽。伺服器在儲存時重新計算(為唯一正確來源)。
輸入:配方行列表(每行含 QtyEntered、SC_Weight、MaterialType、M_ProductSolidContent_ID、Pigment、isHighBoiling)
輸出(FormulaCalcResult):
| 欄位 |
計算方式 |
totalQty |
含固含量行的 QtyEntered 總和 |
scTotal |
含固含量行的 SC_Weight 總和 |
scTotalPct |
scTotal / totalQty x 100 |
mbTotal |
MONO + BIND 類型的 SC_Weight 總和 |
cbTotal |
PS 類型的 (Qty x Pigment / 100) 總和 |
cbTotalSolidPct |
cbTotal / scTotal x 100 |
initiatorTotalSolidPct |
INI SC_Weight / scTotal x 100 |
additiveTotalSolidPct |
ADD SC_Weight / scTotal x 100 |
highBoilingSolventPct |
高沸點溶劑用量 / 溶劑總量 x 100 |
5.2 材料類型
定義於 lib/features/rnd/core/material_type.dart:
| 代碼 |
標籤 |
顏色 |
用途 |
| MONO |
Monomer |
藍色 |
基底單體 |
| BIND |
Binder |
綠色 |
黏結劑 |
| INI |
Initiator |
橙色 |
光引發劑 |
| ADD |
Additive |
紫色 |
添加劑(界面活性劑、整平劑) |
| PAC |
Packaging |
灰色 |
包裝材料 |
| PS |
Pigment Solid |
紅色 |
顏料分散液 |
| SOL |
Solvent |
藍綠色 |
溶劑(含高沸點標記) |
6. PLM 生命週期系統
6.1 生命週期狀態
| 代碼 |
名稱 |
顏色 |
說明 |
| DR |
Draft(草稿) |
灰色 |
初始狀態,可編輯 |
| IR |
In Review(審核中) |
藍色 |
審核中,唯讀 |
| RL |
Released(已發佈) |
綠色 |
已核准可用於生產 |
| OB |
Obsolete(已廢止) |
紅色 |
已棄用,被取代 |
6.2 狀態轉換
DR (Draft) ──────────► IR (In Review)
│
┌─────┴─────┐
▼ ▼
RL (Released) DR (Rejected → Draft)
│
▼
OB (Obsolete) ← 需要 Change Order
6.3 版次追蹤
PLM Repository(PlmRepository)提供:
getRevisions(tableId, rootRecordId) — 含生命週期狀態的版次歷史
getTransitions(stateId) — 從目前狀態可用的轉換
getChangeOrders(revisionId) — 版次的變更命令
版次建立(FormulaRepository.createRevision)會複製配方記錄和所有行,遞增版次號碼(A→B→C)。
7. 響應式佈局
模組使用響應式斷點系統(ResponsiveLayout widget):
| 斷點 |
佈局 |
範例 |
| < 600dp |
手機 |
單欄、tab 導航 |
| 600–900dp |
平板直立 |
自適應 |
| > 900dp |
平板橫向 |
Master-detail 分割 |
秤量配料畫面(Dispensing screen)是最佳範例:在手機上以 Formulas/Batch Tickets 分頁呈現,在平板橫向則透過 MasterDetailLayout 並排顯示。
8. 圖表與視覺化
模組使用 fl_chart 進行資料視覺化:
| 畫面 |
圖表類型 |
用途 |
| Test Trend |
折線圖(Line chart) |
依測試規格顯示測試結果隨時間趨勢 |
| Workload Dashboard |
堆疊長條圖(Stacked bar chart) |
每位研究人員的工時(已指派 vs 進行中) |
| Performance Dashboard |
雷達圖(Radar chart) |
多維度的 KPI 分數 |
| Performance Dashboard |
進度條(Progress bars) |
個別 KPI 達成度 |
| Cost Dashboard |
圓餅圖(Pie chart) |
材料 vs 人工成本分布 |
| Cost Dashboard |
進度條(Progress bar) |
預算使用率 |
9. 路由對應
所有 R&D 路由定義於 lib/features/rnd/rnd_routes.dart,巢狀在 dashboard shell route 下。
| Route Path |
Screen |
Phase |
rnd/formula-list/:projectId |
FormulaListScreen |
1 |
rnd/formula/:id |
FormulaDetailScreen |
1 |
rnd/formula/:id/edit |
FormulaEditorScreen |
1 |
rnd/formula/:id/compare |
FormulaCompareScreen |
1 |
rnd/dispensing |
DispensingScreen |
1 |
rnd/experiment-list/:projectId |
ExperimentListScreen |
2 |
rnd/experiment/:id |
ExperimentDetailScreen |
2 |
rnd/test-result/new?experimentId= |
TestResultEntryScreen |
2 |
rnd/test-result/:id |
TestResultEntryScreen (edit) |
2 |
rnd/test-trends |
TestTrendScreen |
2 |
rnd/kb |
KBSearchScreen |
3a |
rnd/kb/article/new |
ArticleEditorScreen |
3a |
rnd/kb/article/:id |
ArticleViewerScreen |
3a |
rnd/kb/article/:id/edit |
ArticleEditorScreen |
3a |
rnd/formula-compare?ids=1,2,3 |
FormulaComparisonScreen |
3a |
rnd/recommendation |
RecommendationWizardScreen |
3b |
rnd/calibration |
CalibrationScreen |
4a |
rnd/calibration/:equipmentId |
CalibrationScreen |
4a |
rnd/work-orders?projectId= |
WorkOrderListScreen |
4b |
rnd/work-order/:id |
WorkOrderDetailScreen |
4b |
rnd/my-assignments/:userId |
MyAssignmentsScreen |
4b |
rnd/workload |
WorkloadDashboardScreen |
4b |
rnd/performance?userId= |
PerformanceDashboardScreen |
4c |
rnd/cost/:projectId |
CostDashboardScreen |
4d |
9.1 Enhanced Module Registry 選單入口
| Key |
AD_Window_ID |
Route |
Icon |
W:1000904 |
RND Project |
/dashboard/rnd/project |
science |
W:1000905 |
RND Experiment |
/dashboard/rnd/experiment-list/0 |
biotech |
W:1000911 |
Test Trend |
/dashboard/rnd/test-trend |
trending_up |
W:1000912 |
Knowledge Base |
/dashboard/rnd/kb |
menu_book |
W:1000913 |
AI Recommendation |
/dashboard/rnd/recommendation |
auto_awesome |
W:1000914 |
Work Orders |
/dashboard/rnd/work-orders |
assignment |
W:1000915 |
Workload Dashboard |
/dashboard/rnd/workload |
bar_chart |
W:1000916 |
Performance |
/dashboard/rnd/performance |
speed |
R:1000917 |
Project Cost Report |
/dashboard/rnd/cost/0 |
attach_money |
10. 國際化 (i18n)
四國語言切自如,比聯合國翻譯還勤勞。所有使用者可見的字串都使用 S class:
import '../../../../l10n/app_localizations.dart';
final l10n = S.of(context); // Non-nullable,不用 ! 驚嘆號
| 項目 |
說明 |
| 支援語系 |
en、zh_TW、zh、ja |
| R&D Key 前綴 |
rnd*(例:rndProjectTitle、rndFormulaTitle、rndDispensingTitle、rndEquipmentTitle) |
| Template ARB |
lib/l10n/app_zh_TW.arb(新字串先加在這裡) |
| Config 用法 |
title: (l10n) => l10n.featureTitle(延遲求值) |
11. 測試
寫測試是對未來的自己最好的投資——免得改一行 code 倒三個功能。
11.1 單元測試
位於 test/features/rnd/:
| 測試檔案 |
覆蓋範圍 |
formula_calc_service_test.dart |
固含量計算 |
formula_list_notifier_test.dart |
列表狀態、Lifecycle 篩選、分頁 |
11.2 關鍵測試模式
- autoDispose providers:在測試中使用
container.listen() 保持 provider 存活,否則 Riverpod 會把它回收掉,你的測試就在跟空氣說話
- Async notifiers:呼叫 async 方法後一定要
await,再做 assertion。不等結果就檢查,跟考卷還沒發就開始對答案一樣蠢
- Sentinel pattern:copyWith 中的 nullable 欄位用
Object? field = _sentinel,區分「沒傳」和「傳 null」
12. 伺服器外掛(tw.topgiga.rnd)
12.1 結構
src/tw/topgiga/rnd/
├── model/
│ ├── I_RND_*.java # Interface 類別
│ ├── X_RND_*.java # 產生的 Model 類別
│ └── M_RND_*.java # 商業邏輯 Model 類別
├── process/
│ ├── PLMCreateRevision.java
│ ├── PLMTransitionState.java
│ ├── GenerateBatchTicket.java
│ ├── CopyFromExistingFormula.java
│ └── EvaluateTestResult.java
├── setup/
│ └── RNDModelFactory.java # Model + Process 工廠註冊
└── callout/
└── BMCalc.java # 伺服器端配方計算
12.2 DDL 遷移檔
位於 tw.topgiga.rnd/migration/postgresql/:
| 檔案 |
Phase |
Tables |
2026-02-15_Phase1_PLM_Core_Tables.sql |
1 |
PLM_Lifecycle*, PLM_Revision, PLM_ChangeOrder |
2026-02-15_Phase2_Experiment_Tables.sql |
2 |
RND_Experiment, RND_TestSpec, RND_TestResult |
2026-02-15_Phase3_KB_Recommendation_Tables.sql |
3 |
RND_KBArticle, RND_KBTag, RND_Recommendation |
2026-02-15_Phase4_Lab_Dispatch_Tables.sql |
4 |
RND_Equipment, RND_WorkOrder, RND_KPI, RND_Performance |
13. 相依套件
| Package |
用途 |
flutter_riverpod |
狀態管理 |
go_router |
導航路由 |
dio |
HTTP 用戶端 |
fl_chart |
圖表(折線、長條、圓餅、雷達) |
flutter_markdown |
Markdown 渲染(KB 文章) |
14. 已知限制與未來工作
- AI 推薦從用戶端直接呼叫 Claude API(需要在設定中提供 API key)。應考慮伺服器端代理以提升安全性。
- 成本報表使用硬編碼的時薪($50)。應整合 HR 模組取得實際費率。
- 離線模式:瀏覽已快取。所有編輯操作需要網路連線。
- 全文搜尋:知識庫依賴伺服器端 PostgreSQL 的
gin 索引(RND_KBArticle)。
- 設備預約整合:
S_Resource_ID 欄位連結到 iDempiere Booking,但尚未與 App 的預約功能整合。
15. 搖一搖回報 (Shake-to-Report)
搖手機不是因為生氣,是因為我們提供了最暴力但最有效的 Bug 回報方式——搖一下,截圖 + 裝置資訊自動收集,直接建立 R_Request。比起在 LINE 群組打「那個功能怪怪的」,這個方式顯然更有建設性。
15.1 相關套件
| Package |
用途 |
shake ^3.0.0 |
加速度計搖動偵測 |
screenshot ^3.0.0 |
Widget 截圖 |
device_info_plus ^11.0.0 |
裝置型號/OS 資訊 |
package_info_plus ^8.0.0 |
App 版本資訊 |
15.2 架構
MaterialApp.router
└── builder:
└── ShakeFeedbackWrapper ← Screenshot + ShakeDetector
└── _OfflineBannerWrapper
└── Navigator (GoRouter)
ShakeFeedbackWrapper 放在 MaterialApp.router 的 builder 回呼裡,確保能存取 Localizations、Navigator 和 Theme。
15.3 檔案結構
lib/features/feedback/
├── data/
│ ├── device_info_collector.dart # 靜態工具:收集裝置/App 資訊
│ └── feedback_repository.dart # 建立 R_Request + 上傳截圖
├── domain/
│ └── feedback_notifier.dart # FeedbackState + StateNotifier
├── presentation/
│ └── feedback_sheet.dart # Modal BottomSheet UI
└── shake/
├── shake_feedback_settings.dart # 開關偏好(SharedPreferences)
└── shake_feedback_wrapper.dart # App 層級包裝器
15.4 資料流
使用者搖手機
│
▼
ShakeDetector.onPhoneShake
│ ← 檢查:啟用?冷卻期?Sheet 已開啟?
▼
ScreenshotController.capture()
│
▼
DeviceInfoCollector.collect()
│ ← 裝置型號、OS、App 版本、路由、語系
▼
FeedbackSheet.show()
│ ← 使用者填寫:問題類型 + 描述
▼
FeedbackNotifier.submitFeedback()
│
▼
FeedbackRepository.submitFeedback()
├── ApiClient.createRecord('R_Request', data)
└── UploadApi.uploadAndAttach(screenshot.png)
15.5 R_Request 欄位對應
| R_Request 欄位 |
資料來源 |
Summary |
[問題類型] 描述前 50 字元 |
Result |
使用者描述 + 自動收集的裝置資訊 |
R_RequestType_ID |
(尚未對應——類型標籤僅出現在 Summary) |
| 附件 |
截圖 PNG,透過 Upload API 上傳 |
15.6 搖動行為參數
| 參數 |
值 |
| 靈敏度 |
shakeThresholdGravity: 2.7(預設值) |
| 冷卻時間 |
3 秒(防止連續觸發) |
| 僅前景 |
App 進背景時暫停,回前景恢復 |
| Sheet 防護 |
回報 Sheet 已開啟時忽略搖動 |
| 開關 |
SharedPreferences key shake_feedback_enabled,預設 true |
15.7 整合點
| 檔案 |
變更 |
pubspec.yaml |
新增 shake、screenshot、device_info_plus、package_info_plus |
lib/app.dart |
ShakeFeedbackWrapper 加入 MaterialApp.router builder |
settings_tab.dart |
設定頁面的「系統」區塊加入開關 |
lib/l10n/app_*.arb |
4 種語言各 15 個回饋字串 |
15.8 錯誤處理
| 情境 |
行為 |
| 截圖失敗 |
表單正常開啟但沒有預覽,允許純文字提交(有比沒有好) |
| 網路錯誤 |
顯示本地化錯誤 SnackBar,表單保持開啟可重試 |
| R_Request 建立失敗 |
顯示 AppException.localizedMessage(l10n) 的 SnackBar |
🌐 English — New Sections (10, 11, 15)
10. Internationalization (i18n)
Four languages, zero excuses for missing translations. All user-facing strings use the S class:
import '../../../../l10n/app_localizations.dart';
final l10n = S.of(context); // Non-nullable, no ! needed
| Item |
Details |
| Supported Locales |
en, zh_TW, zh, ja |
| R&D Key Prefix |
rnd* (e.g., rndProjectTitle, rndFormulaTitle, rndDispensingTitle) |
| Template ARB |
lib/l10n/app_zh_TW.arb (add new keys here first) |
| Config Usage |
title: (l10n) => l10n.featureTitle (lazy evaluation) |
11. Testing
Writing tests is the best investment you can make for your future self — unless you enjoy debugging three broken features after changing one line of code.
11.1 Unit Tests
Located at test/features/rnd/:
| Test File |
Coverage |
formula_calc_service_test.dart |
Solid content calculation |
formula_list_notifier_test.dart |
List state, lifecycle filter, pagination |
11.2 Key Testing Patterns
- autoDispose providers: Use
container.listen() to keep providers alive in tests — otherwise Riverpod garbage-collects them and your test is talking to thin air
- Async notifiers: Always
await async method calls before asserting. Checking results before they arrive is like grading an exam before handing it out.
- Sentinel pattern: Use
Object? field = _sentinel for nullable copyWith fields to distinguish “not passed” from “passed null”
15. Shake-to-Report Feedback
Shaking your phone is not an anger management technique — it is the most violent yet effective bug reporting method. One shake captures a screenshot, collects device info, and creates an R_Request in iDempiere.
Architecture
MaterialApp.router
└── builder:
└── ShakeFeedbackWrapper ← Screenshot + ShakeDetector
└── _OfflineBannerWrapper
└── Navigator (GoRouter)
Data Flow
User shakes phone
↓
ShakeDetector.onPhoneShake (check: enabled? cooldown? sheet open?)
↓
ScreenshotController.capture()
↓
DeviceInfoCollector.collect() (device model, OS, app version, route, locale)
↓
FeedbackSheet.show() (user fills: problem type + description)
↓
FeedbackRepository.submitFeedback()
├── ApiClient.createRecord('R_Request', data)
└── UploadApi.uploadAndAttach(screenshot.png)
R_Request Field Mapping
| Field |
Source |
Summary |
[Problem Type] first 50 chars |
Result |
User description + auto-collected device info |
| Attachment |
Screenshot PNG via Upload API |
Shake Behavior
| Parameter |
Value |
| Threshold |
shakeThresholdGravity: 2.7 |
| Cooldown |
3 seconds between triggers |
| Foreground only |
Pauses on background, resumes on foreground |
| Toggle |
SharedPreferences key shake_feedback_enabled, default true |
Error Handling
| Scenario |
Behavior |
| Screenshot fails |
Form opens without preview, text-only submission |
| Network error |
SnackBar with error, form stays open for retry |
| R_Request creation fails |
SnackBar with AppException.localizedMessage |
🇯🇵 日本語 — 新セクション(10, 11, 15)
10. 国際化 (i18n)
4言語対応、国連翻訳者より勤勉です。全てのユーザー向け文字列は S クラスを使用:
import '../../../../l10n/app_localizations.dart';
final l10n = S.of(context); // Non-nullable、! 不要
| 項目 |
詳細 |
| 対応ロケール |
en、zh_TW、zh、ja |
| R&D キー接頭辞 |
rnd*(例:rndProjectTitle、rndFormulaTitle) |
| テンプレート ARB |
lib/l10n/app_zh_TW.arb(新しいキーはここに最初に追加) |
| Config 用法 |
title: (l10n) => l10n.featureTitle(遅延評価) |
11. テスト
テストを書くのは未来の自分への最良の投資です — 1行変更して3機能壊すデバッグが楽しいなら別ですが。
11.1 ユニットテスト
test/features/rnd/ に配置:
| テストファイル |
カバレッジ |
formula_calc_service_test.dart |
固形分計算 |
formula_list_notifier_test.dart |
リスト状態、ライフサイクルフィルター、ページネーション |
11.2 重要なテストパターン
- autoDispose providers:テスト内で
container.listen() を使って provider を生存させる — さもなければ Riverpod がガベージコレクトして、テストは虚空に話しかけることに
- Async notifiers:async メソッド呼び出し後は必ず
await してからアサーション。試験を配る前に採点するようなもの。
- Sentinel pattern:nullable な copyWith フィールドに
Object? field = _sentinel を使用し、「未指定」と「null を指定」を区別
15. シェイク・トゥ・レポート(搖一搖バグ報告)
スマホを振るのは怒りのせいではありません — 最も暴力的だが最も効果的なバグ報告方法です。一振りでスクリーンショットを撮影、デバイス情報を収集し、iDempiere に R_Request を作成します。
アーキテクチャ
MaterialApp.router
└── builder:
└── ShakeFeedbackWrapper ← Screenshot + ShakeDetector
└── _OfflineBannerWrapper
└── Navigator (GoRouter)
データフロー
ユーザーがスマホを振る
↓
ShakeDetector.onPhoneShake(有効?クールダウン中?Sheet 表示中?)
↓
ScreenshotController.capture()
↓
DeviceInfoCollector.collect()(デバイスモデル、OS、アプリバージョン、ルート、ロケール)
↓
FeedbackSheet.show()(ユーザーが入力:問題タイプ + 説明)
↓
FeedbackRepository.submitFeedback()
├── ApiClient.createRecord('R_Request', data)
└── UploadApi.uploadAndAttach(screenshot.png)
R_Request フィールドマッピング
| フィールド |
ソース |
Summary |
[問題タイプ] 説明の最初50文字 |
Result |
ユーザー説明 + 自動収集されたデバイス情報 |
| 添付ファイル |
スクリーンショット PNG(Upload API 経由) |
シェイク動作パラメータ
| パラメータ |
値 |
| しきい値 |
shakeThresholdGravity: 2.7 |
| クールダウン |
トリガー間隔 3 秒 |
| フォアグラウンドのみ |
バックグラウンドで一時停止、復帰で再開 |
| トグル |
SharedPreferences キー shake_feedback_enabled、デフォルト true |
エラーハンドリング
| シナリオ |
動作 |
| スクリーンショット失敗 |
プレビューなしでフォーム表示、テキストのみ送信可能 |
| ネットワークエラー |
エラー SnackBar を表示、リトライ可能 |
| R_Request 作成失敗 |
AppException.localizedMessage の SnackBar |