"Flutter"
- Riverpod 的 reactive 邊界
頁面用了 Riverpod 卻對某些變化沒反應、或 reactive 行為在特定時機炸掉時使用。Riverpod 的 reactive 保證只覆蓋 provider 圖的內部——排查沿著圖的邊界走:變化在圖上嗎、在哪個容器的圖上、節點還活著嗎。
- T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽
Flutter app 用 Uint8List 發送 WS 資料走 binary frame,ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type,192 個 test 全過但實機無回應
- U.C1 Terminal 畫面五個狀態零個退出路徑
Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態,每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去
- Widget test 的狀態覆蓋策略
從畫面狀態矩陣推導 widget test case — 每個狀態的顯示、操作、退出路徑都是獨立的斷言目標
- 流程測試基礎設施
在 Flutter 專案建立流程測試時碰到的 Dart 實作限制:控制器能不能在 headless 環境立起來、binding 怎麼跟真實網路測試共存、測試輸出雜訊怎麼治理、假後端的回應資料走什麼路徑序列化——限制形成一條建置鏈,前一個的答案決定後一個的形態
- Flutter GoRouter 導航設計
GoRouter 的路由定義、導航 API(go / push / pushReplacement)、redirect 機制和 ShellRoute 的使用場景
- Flutter 平台適配
Isolate 安全、Platform channel 攔截、app lifecycle 事件 — Flutter SDK 的平台特殊考量
- U.C2 biometricOnly=true 無密碼 fallback
Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住,沒有替代路徑
- 導航路徑 test
Back 按鈕、route 可達性、go vs push 語意 — 驗證使用者能從任何畫面回到預期的位置
- 值物件的 Dart 實作路徑
一個領域值該不該脫離裸的通用型別、以及在 Dart 用哪種載體實作時使用。手寫 immutable class、freezed 產生器、extension type 零成本包裝的成本結構不同——欄位數、要不要 runtime 身份、boilerplate 容忍度決定選哪條,以及從原始型別遷移過去怎麼鎖住行為不變。
- U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField
Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField + 6 個參數(enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted),全是散落 hotfix
- T.C4 Client-side log 缺失導致 debug 只能靠實機盲測
Flutter app 六個核心元件中只有兩個有 log(且全是 W2 hotfix 補的),連線失敗時開發者無法從任何 log 判斷失敗發生在哪一步 — 被迫用最昂貴的 debug 方式:插拔裝置反覆測試
- U.C4 首頁缺配對入口按鈕、導航流未完整列出
Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作(UC-02 連線)、遺漏了首次操作(UC-01 配對)的入口
- U.C5 匯出按鈕按下零回饋 — 狀態機完備但 UI 沒接線
Flutter app 匯出設定頁的確認匯出按鈕 onPressed 是空 callback,按下畫面毫無變化 — 使用者無法分辨匯出成功、進行中、還是功能根本沒做。ViewModel 的 idle/inProgress/completed/failed 狀態機早已完備,缺的只是頁面接線與三層回饋
- go vs push vs pushReplacement 的 UX 語意表
三種導航方法對堆疊、back 行為、使用者心理模型的影響 — 選擇依據是使用者的意圖而非技術方便
- U.C6 加書後返回不刷新統計 — 只設計了進入時載入
Flutter app 資料管理頁的書籍統計只在 initState 載入一次,從頁面進入新增流程加書後 pop 返回,統計停留在舊值(仍顯示無書目),要回首頁再進入才更新。根因是 happy-path-only 反模式的資料版本:設計了「進入時載入」,沒設計「資料變更時的轉移」
- U.C7 商品條碼的誤導性查無結果 — 可本地判定的錯誤讓使用者白等 API
Flutter app 的 ISBN 掃描器接受所有 EAN-13 條碼,掃到一般商品條碼(非 978/979 開頭)時送 API 查詢,2 秒後回「查無結果」— 訊息誠實但誤導,使用者以為書找不到,實際上是掃錯了條碼。正確回饋是本地立即判定「這不是書籍條碼」
- U.C8 標籤行只有箭頭可點 — 觸控目標小於視覺單元
列表行的展開/收合看起來整行可點、實機測試點文字卻無反應時使用。gesture 只掛在尾端箭頭 icon 上、整行的視覺暗示範圍遠大於實際可點區域,使用者體感等同功能壞掉。
- U.C15 切換按鈕顯示目標模式被讀成當前狀態 — 標籤語意歧義
模式切換按鈕的文字被使用者讀成「現在的狀態」而非「按了會去哪」— 單顆文字按鈕無法自證標籤是現態還是目標,歧義是結構性的,解法是把狀態顯示與切換動作的責任拆開
- U.C16 篩選列截斷被讀成遮蔽 — 水平溢出沒有捲動提示
水平清單超出畫面寬度、使用者回報「被別的元件蓋住」或找不到後面的選項時使用。截斷若無 affordance(漸層、部分露出、箭頭),使用者讀到的是「壞了」不是「可以捲」
- U.C17 選中態換底色不換文字色 — 對比沒有成對設計
選中的 chip / tab / 按鈕文字看不清楚:選中態是「底色 × 文字色」的成對設計,只指定其中一半、另一半走元件庫預設,組合對比從未被驗證
- U.C18 狀態圖示被當成按鈕點 — 非互動指示與動作按鈕同形
使用者回報「某個按鈕點了沒反應」、查程式發現那不是按鈕時使用。非互動的狀態圖示與動作按鈕同列混排且形態近似(描邊 vs 實心),使用者無法區分可點性
- U.C19 「已選擇」計數被版面擠壓成省略號 — 回饋死在 layout
狀態文字顯示成「...」、資料層查起來卻一切正常 — 回饋鏈的最後一哩是版面,flex 寬度競爭把關鍵計數整串壓成省略號,state 正確、使用者拿到零資訊
- U.C20 管理模式操作全是佔位 — dev toast 讓未接線看起來有反應
UI 有按鈕、domain 層功能也寫完、按下去只有開發提示或 log:佔位 handler 比沒有按鈕更糟 — 按鈕的存在承諾功能存在,dev toast 讓開發自測「有反應」、掩蓋未接線
- TestWidgetsFlutterBinding 會擋掉真實網路:真實後端測試與流程測試的檔案級隔離
flutter_test 的 binding 初始化後會把 HttpClient 換成回 400 的假件——需要真實網路的後端驗證測試不可初始化 binding,也因此不可 import 任何會初始化 binding 的 harness。靠測試檔案各自跑在獨立 isolate 的特性,兩種測試在同一個目錄共存。
- 有狀態假後端用真實模型序列化回應:手寫 JSON fixture 會重踩產品已解決的問題
流程測試的假後端持有 freezed 模型物件、以 toJson 序列化回應,讓服務層走完整的反序列化鏈。對照組是手寫 JSON fixture——同一批測試裡的 raw 寫法重踩了一次產品早已內建處理的分頁包裝,證明「回應形狀的知識」應該只存在一份。
- 測試輸出的雜訊治理:預期的環境狀態不該走例外路徑
測試輸出長期印著兩行「已知無害」的錯誤——相機偵測 MissingPluginException、toast 套件的 assert fallback。已知雜訊會訓練人忽略輸出,新警報混在裡面就被過濾掉。修法是把「預期的環境狀態」變成前置判斷(channel mock 回空、無畫面早退),讓例外路徑只剩真正的例外。
- 讓 UI 控制器在 headless 測試立起來:platform channel mock、no-op 子類與 postFrameCallback 的手工補位
流程測試要驅動真實編排,而編排住在 UI 控制器裡——能不能在無畫面的測試環境把控制器立起來,決定整個測試套件的形態。先用 spike 驗證閘門、再逐項中和平台耦合,讓控制器在 headless 環境可建構。
- StreamProvider 包 repository watch stream — broadcast、初始值、dispose 實作點
repository 要補 Stream 觀測出口、接給 Riverpod 消費時使用。訂閱模型選 broadcast 還是單訂閱、新訂閱者拿不拿得到當下狀態、controller 誰負責關——每個問題各有一個會靜默失效的預設答案。
- 手寫 dispose() 沒有呼叫者 — Notifier 的依賴與清理都歸 build() 管
Notifier 用建構子注入依賴、或手寫 dispose() 釋放 Timer 與訂閱時使用。Notifier 的建構與銷毀都由容器管理——UI 不會呼叫 dispose()、掛在方法上的清理等於沒掛;依賴在 build() 內 ref.watch、清理在 ref.onDispose 註冊。
- 加書後統計不刷新 — ref.watch 觀察的是 provider 圖、不是資料庫
頁面用了 Riverpod 卻在資料寫入後不更新、或發現自己在導航返回點補 loadData()、用 EventBus 事件觸發 reload 時使用。ref.watch 的 reactive 範圍是 provider 圖上的狀態變化;資料庫寫入不在圖上,補償刷新的出現就是這個缺口的訊號。
- 一行查詢放哪、一個測試留不留:結構改動後的兩次「還需要存在嗎」
修完一個應收金額 bug 收尾時,兩個『還需不需要』的問題浮出來。一段『過濾出已結帳品項』的查詢該掛哪一層——inline 在 widget 是洩漏、開一個 service 是儀式,判準是『查詢誰擁有的資料就掛給誰』,答案是既有的 repository。一個為了鎖住修復而寫的測試該不該留——當修復是靠刪掉舊函式、移除參數達成時,那個 bug 被結構免疫了,測試變贅述;判準是『這測試守的失敗模式,結構是否已經替它擋掉』。
- 測試全綠、功能失聯:五個 runtime 問題與組裝層的接線缺口
113 張票收尾全綠的版本,實機測試找出五個問題:路由指向佔位頁、provider 佔位 throw、按鈕空 callback,加上兩個平台語意差異。單元測試的 mock override 是正當的測試 seam、同時是遮蔽接線斷裂的來源——記下三層共振的機制、反向追溯設計文件的結果、以及修補時規格層/測試層/發版層的分層落點。
- 「978ABC」被拒的理由寫著長度不對 — 驗證的兩層分工與順序陷阱
輸入驗證有兩層職責:建構期不變式守「這個物件能不能存在」、無狀態 validator 守「使用者輸入對不對」,混在一起會讓測試建不出 fixture、錯誤訊息歸錯類。順序陷阱:先標準化再檢查等於先銷毀證據再診斷——含字母的 ISBN 被削成三位數、錯誤訊息說長度不對。
- 「該收多少錢」抽成 pure function — IO 在邊界、領域計算在核心
多個畫面都要顯示「未結帳的份數與金額」時,把計算抽成無 IO 的 pure function:資料由 caller 從 repository 拿好傳入、函式只做合併 / 扣減 / 折扣運算。含合併鍵要跟同一性定義同維度的陷阱、兩層折扣各自 clamp 的邊界、以及用註解預留擴充點讓未來規則接入不動本體。
- 1000 本書、1001 次 SQL — N+1 查詢藏在 async mapper 裡
N+1 查詢由兩個各自合理的函式組合而成:單筆轉換函式順便查關聯(mapper 做 IO)、列表方法用 Future.wait 把它乘以 N。修法是 IO 上移——一次批次 IN 查詢、記憶體組裝、轉換函式變純。mapper 簽名是 async 就是訊號;開發期資料量小、惡化是上線後隨資料成長的乘法。
- 1101 行自建測試基礎設施、重構刪掉 82.5% — 過度工程的三種形態
自建測試基礎設施前先問框架的標準做法是什麼:mock 純資料物件、helper 帶並發鎖與記憶體洩漏防護、mock 放進 lib/ 進生產依賴圖,三種形態都在重新發明 Riverpod overrideWith 一行就有的東西。精緻的設計文件不是價值證明——它可以精心規劃一個不需要存在的系統。
- 16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修
測試失敗數超過十個時逐個修是錯的順序:先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法;每類的症狀特徵字串可以建成索引讓下批失敗直接對號。
- 16 種支付渠道、4 種行為分類 — 分層 enum:保真層與行為層的粒度分工
同一個分類系統要同時服務序列化(要無損)跟 UI 行為分流(要粗粒度)時,單一 enum 選哪個粒度都錯。解法是分層:保真層無損對齊後端完整列舉、行為層收斂成行為真正分歧的少數大類、層間用 exhaustive switch 衍生——粒度轉換獲得編譯期保證。
- 88 行拆成 13 個函式、90 行決定不拆 — 函式長度是症狀、職責才是診斷
同一個團隊、同一條 5-10 行規範、兩個 90 行上下的函式,一個拆一個保留、兩個決定都對。判準在行數之外:職責混雜(初始化/迴圈/儲存/統計擠一起、巢狀五層)拆之,完整業務流程(步驟多但答案只有一個)留之。含 _ImportProgress 收斂參數列與測試耦合行為的守護。
- App 永遠卡在載入畫面 — Riverpod 的 provider 是配方、容器才持有狀態
main() 自建 ProviderContainer 對它觸發初始化、UI 跑在 runApp 的 ProviderScope 裡——兩個容器各持一份 provider 狀態、互不相通,UI 監聽的那份永遠停在初始值。Riverpod 的全域 provider 宣告只是配方、狀態屬於容器實例;跨容器操作是靜默的無效操作。
- await 回來的時候、頁面已經關了 — UnmountedRefException 與 16 個不抽象的檢查點
長 async 流程的每個 await 都是一個 gap:等待期間使用者可能離開、Notifier 被 dispose、回來再寫 state 就炸 UnmountedRefException。修法是每個 gap 後檢查 ref.mounted——而且刻意不抽成 helper:明確的檢查點讓 review 看得見哪個 gap 有守。含「評估必跑、可決定不重構」的技術債處置。
- BookImported 不能寫成 ImportBook — 事件命名的過去式是語意類別、不是風格
domain event 用過去式命名(BookImported)因為事件是已發生的事實;動詞開頭(ImportBook)是命令的形狀、訂閱者會誤讀成指令。字尾清單的自動檢查抓不住不規則動詞——偵測可機械化、判定要看語意。附一次版本終止:工作日誌宣稱的命名問題實際早已不存在、執行前驗前提省下整輪流程。
- copyWith 是逃生口,不是設計 — 從一個測試 bug 追到 entity 稽核軌跡的洞
copyWith 對純資料載體是正確工具,對有領域方法的 entity 是繞過不變式的逃生口。從一個 3 字元 ID 觸發的例外,追出同族語意錯誤、被繞過的領域方法、以及從未被強制的註解約束。
- Domain 層的 947 處硬編碼中文 — 訊息代碼跟顯示文字的分層責任
domain 層的 enum 或 getter 直接回傳 UI 顯示字串時,多語言支援與分層原則同時失守。修法是 domain 只回訊息代碼與結構化資料、UI 層用 translator extension 翻譯;遷移排序從最小模組先行驗證模式。適用於盤點「這個字串屬於領域事實還是呈現」的情境。
- Exception 型別綁 ErrorCategory 的建構不變式 — 以及合法需求撞上不變式的時刻
把「錯誤代碼必須屬於對應分類」做成建構期不變式,錯誤分類錯亂會變成測試失敗而不是靜默混亂;同一批修復出現三種形態——換對值、換精確值、以及改繼承逃離約束。第三種是分類學本身的訊號:一個 domain 的錯誤天生橫跨技術分類時,分類軸跟階層軸不正交。
- mock 要配置 55 個方法、實際只用 5 個 — 測試痛是介面設計痛的探針
service 測試的 mock 負擔正比於它依賴的介面寬度:依賴四個大介面共 55 個方法、實際呼叫 5 個,91% 的 mock 配置是純浪費、還會炸 MissingStubError。修法是介面隔離——抽出只含實際使用方法的 Port,讓 mock 縮到跟真實依賴一樣窄;為未來預留的方法用 TODO 標記啟用時機。
- SQLite 只吃三種型別 — value object 在持久化邊界的序列化契約
把 value object 直接塞給 sqflite 會炸 Invalid argument——SQLite 只接受 num / String / Uint8List,VO 必須在 repository 邊界拆成基本型別、讀回時重建。用 toString/fromString 當轉換通道是權宜:它依賴兩者對稱這條沒人強制的隱性契約,正解是語意明確的序列化方法。
- Value Object 的封裝擺盪:從全移除、完全封裝、到加回 .value getter
VO 的封裝邊界在兩個極端之間來回——純字串(零封裝)跟完全封裝(禁止取原始值)各有成立的理由、也各自撞牆。穩態是給原始值一個有語意的官方出口,而不是把「取原始值」本身當違規。含 176 個編譯錯誤的工作量低估、以及「相容性介面」作為理想撤退訊號的判讀。
- 文件裡的扁平 Product、程式碼裡的雙層聚合 — 宣稱型文件的半衰期
refactor 總結文件記的是決策時刻的快照:扁平 Product(一商品一價一庫存)在真實 POS 業務下演化成 Product + ProductSpecification 雙層、價格三種下沉到規格。欄位放聚合根還是子層的判準是「兩個規格會不會不同」;文件預言的需求全中、預言的結構全錯——這正是先蓋結構會蓋錯的實證。
- 功能「完成」、測試全過、資料從未落地 — 持久化迴圈是驗收的盲區
domain 功能的測試可以全綠、而它的資料從未被序列化、資料庫沒有對應的表——單元測試都在記憶體內驗證行為、沒有一條測試走「存進去、重建、讀出來」的迴圈。驗收定義要含 roundtrip;entity 欄位與 schema 欄位的差集是靜默資料失真的清單。
- 只活在結帳流程裡的領域物件 — ephemeral model 與「Rx 外殼、immutable 內核」
流程型狀態(結帳中的輸入金額、支付方式、會員)建模成生命週期等於流程的 ephemeral 物件:結完即丟、下次全新,殘留狀態忘記重置的 bug 被結構性消滅。實作形態是 reactive 外殼包 immutable 內核——對外只開語意化變更方法、每次變更是原子的狀態替換。
- 用事件做同步查詢、等於手工重建 RPC — 跨 domain 解耦的完整帳單
domain 直接查另一個 domain 的 repository 違反依賴方向;改事件驅動的 request/response 解了耦、但帳單具體:correlation id 配對並發、timeout 機制、三條錯誤路徑——都是同步呼叫免費附贈的東西。判準是需要解耦「依賴方向」還是「時間與部署」:前者用消費端 Port 就夠、後者才值得付事件的價。
- 同一個子系統膨脹兩次:異步查詢系統的過度設計震盪
過度設計會復發、且兩輪的機制不同:設計期的膨脹來自想像的需求(別層已處理的重試、用不到的優先級佇列),迭代期的膨脹來自不刪的舊版本(三個實作並存、狀態多處追蹤)。偽需求的檢驗法是問「這個能力已經有別層在做嗎」。
- 同一個品項、四個 model — value object 什麼時候該升級成 entity
同一個業務概念要不要拆成多個 model、value object 什麼時候該升級成 entity——判準是操作需不需要 identity-based 回寫。以 POS 品項從點選、掛單、結算到歷史訂單的四階段模型為例,含 snapshot 與 live reference 的凍結時機。
- 同一個類別被判成兩個型別 — Dart import 的相對路徑與 package 路徑衝突
錯誤訊息出現 LibraryId/*1*/ 與 LibraryId/*2*/、兩行 from 一個指 lib/ 一個指 package:,就是同一個檔案被兩種 URI 匯入——Dart 的 library 身份由匯入 URI 決定、不由檔案決定,相對路徑跨進 lib/ 會製造出平行的型別宇宙。修法是全案統一 package 路徑並用 lint 規則封死。
- 兩個 domain 各自實作同一個 API service — 100% 覆蓋率的假象
同名 service 在多個 domain 各自實作時,覆蓋率數字會失去意義:每份實作各測各的、mock 各有介面,統一的行為從未被測過。重複實作是上游訊號——規劃文件沒抽出跨 domain 的共同技術需求;單檔品質審查看不到跨檔重複。
- 兩個 ImportResult 各自都合理 — 傘狀名的碰撞與做一半的重命名
同一個 domain 裡兩個類別都叫 ImportResult:一個是驗證結果、一個是操作結果,各自誕生時都合理。修法是依語意責任命名(ImportValidationResult);而重命名是一組原子操作——類別名、檔名、import、文件宣稱,做一半留下的不一致比不做更迷惑,且宣稱完成與實際完成的漂移要靠稽核抓。
- 取個原始值有四種寫法 — VO 的 toString 洩漏與 accessor 不一致
value object 家族的取值 accessor 各自為政(displayValue、toString、裸欄位)時,殺傷力會在測試層爆開:自定義 toString 讓裸字串斷言全數過期、四種取法讓修復者自己都寫錯。止血是 helper 函數庫集中取值知識、根治是家族統一 accessor 命名。
- 金額型別的三段遷移:double、Decimal、再到 Money extension type
金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」;用 Dart extension type 包成 Money 之後,型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。
- 宣告回歸原生 Exception、三個版本後階層重生 — 需求不死、只是換宿主
「砍掉過度設計、回歸原生」的重構把實作砍了、沒盤點實作回應的需求:錯誤要序列化、要使用者訊息、要分類路由,這些消費者還在,於是基類與階層在新架構裡原地長回來。砍之前分「有消費者的機能」與「沒有的」,口號式目標不可驗收、需求清單可以。
- 相同 ISBN 的兩本書、相似度只有 0.67 — 加權平均稀釋 identity 訊號
把 identity 級訊號(ISBN 相同幾乎等於同一本書)跟 fuzzy 級訊號(標題像、作者像)丟進同一個加權平均,確定性會被稀釋到閾值以下。修法是訊號分級:identity 訊號匹配就短路給高分、fuzzy 訊號才進加權池;且 identity 比對前要先正規化——ISBN-10 與 ISBN-13 是同一身分的兩種表示。
- 紅燈在量什麼 — 測試訊號的三層失真:斷言、量測、環境
「全套件降至 0」有意義的前置條件是紅燈只反映程式缺陷。三層各自會失真:絕對計時斷言量的是機器負載、compact reporter 高並行下行覆寫產生假陰性、fresh checkout 缺 gitignored 生成產物讓整包結果不可信。含 flaky 判定的取樣門檻與對照實驗定歸因的做法。
- 核心 entity 重寫、140+ 檔消費端不動 — Deprecated Getter Facade 的過渡設計
重寫被百餘檔引用的核心 entity 時,直接改會同時打爆全部消費端、長期分支的 merge 成本隨時間暴漲。第三條路是 facade:舊欄位保留為 deprecated getter、內部從新結構回讀,消費端零修改編譯通過、@Deprecated 讓編譯器自動列出遷移清單、再逐波清償。facade 要配退場計畫、否則就是永久相容層。
- 桌子跟購物車是兩個聚合 — 從「提前結帳」推導生命週期解耦
兩個業務資源該綁死成一對一、還是解耦成獨立生命週期加綁定關係——判準是有沒有業務操作需要其中一方獨立存活。以 POS 的提前結帳、純佔桌、外賣單推導桌位與購物車的聚合邊界,含組合空間大於業務空間時的非法組合封鎖。
- 產品碼自己是 mock — ViewModel 假實作通過了 15 個測試
ViewModel 用 Future.delayed 加硬編碼資料交付、狀態轉換測試全綠,因為測試耦合的是狀態機、不是業務效果。假實作被當成完成的基礎,直到 63 個測試「寫不出來」才現形——測試的可寫性是實作真實性的探針。
- 測「不變」、不測「正確」 — characterization test 當遷移安全網
大規模型別遷移前,對著舊實作寫一批鎖住現有行為的測試——包括看起來像 bug 的邊界怪癖也照鎖,遷移後全綠證明「換底沒改行為」。正確性是另一批測試的職責、混在一起紅燈就無法歸因。附測試環境的原生依賴三個斷點(FFI、plugin、late init)與替身解法。
- 會員身分、計價、支付方式必須一起換 — 耦合欄位的原子切換
多個狀態欄位被同一條業務規則綁住時,分開的 setter 會製造不一致的中間態;把切換收成單一方法、一次狀態更新內同步全部欄位,並注意衍生值重算的順序。以 POS 結帳的會員登出重算為例,含不變式收進 model 的 canCheckout 設計。
- 溢出 714px、22 個測試同時紅 — 單點修復與規範化的分界
Row 裡未受 Flexible 包裹的固定尺寸元件在窄約束下溢出;同型失敗一次爆 22 個時,正確的產出不是 22 個修復、是一份 overflow 預防規範(反模式清單 + 決策樹 + 測試檢查項)。含測試環境小尺寸是 feature、以及 stale ticket 先考古再執行的教訓。
- 遷移計畫有寫入、有消費、缺讀出 — read-path 缺口與 fixture 假綠
資料模型遷移的通路要三段齊:寫入 backfill、讀取路徑、消費端 API。缺讀出那段時,新 API 拿到的永遠是空集合——而消費端測試的 fixture 自己建物件、不走真實讀取路徑,測試全綠掩蓋 runtime 靜默失效。依賴圖只列「誰先做」不列語意前提時,dashboard 的 ready 是假訊號。
- Flutter scheduleFrame():按需 render 的最底層原語
手動觸發重繪、或釐清 setState / 動畫 / markNeedsPaint 為何最終都要向引擎要一個 frame 時,回頭理解這個按需 render 的底層原語。
- Flutter 音量控制:App 自己的音量 vs 系統音量
釐清 Flutter/Android 兩種層次的音量——播放器各自的音量(App 免 plugin 就能控制自己的聲音)與裝置系統媒體音量(全域、需 plugin),以及為什麼多數情況不該從 App 去改系統音量。
- Flutter 畫面落後邏輯狀態(log 正確、畫面不符)— 重繪訊號排查與心跳做法
Flutter 畫面落後邏輯狀態(log 正確、畫面不符),含 platform view / 外部 texture 的渲染訊號沒進 frame 排程時回來讀。
- Widget 子類重新宣告 key — 遮蔽父類屬性與 duplicate key 風險
在 StatelessWidget 子類中重新宣告 final Key? key,會遮蔽 Widget 繼承的 key 屬性,產生兩份儲存槽。若再把同一個 key 散播給同層的多個 sibling,rebuild 時 Flutter 會拋 duplicate key 錯。
- 192 個測試全過、實機全壞:Mock 遮蔽真實行為的三層測試策略
unit test 全綠、實機部署後功能整片壞掉。mock-only 策略的結構盲區(text vs binary frame、缺 auth handshake、ANSI 多樣性被 FakeWebSocketChannel 遮蔽),以及分層測試各抓什麼、各遮蔽什麼。
- 每個畫面都需要出口:畫面狀態機設計與 UX 導航的系統性方法
實機測到某畫面沒有返回或退出按鈕、使用者被困住。根因是企劃沒系統列出每個畫面的狀態與可用操作;用畫面狀態矩陣確保每個狀態都有明確出口。
- 高階函式的適用判準:流程固定、變化點單一且開放——以 Flutter 設定更新比較 typedef 改寫前後
高階函式的適用判準(流程固定、變化點單一且開放)與裸函式型別 vs typedef 的可讀性取捨並排比較。
- 寫測試時 sync try-catch 接不到 BotToast 的 async 錯誤:fire-and-forget API 的接管設計
測試裡 sync try-catch 接不到錯誤,或 fire-and-forget API 從 async gap 後拋 `LateInitializationError`。用 runZonedGuarded 同時罩 sync 與 async 失敗路徑,含 fallback 訊息 signature 設計。
- flutter devices 卡住的訊號:device 數從 N 變 N-1 與 emulator 半活
`flutter devices` / `flutter run` 卡住又印 `Error -2 retrieving device properties` 時回來看。根因是 Android emulator 半活狀態,附恢復順序。
- Freezed 的三層結構解剖:with、_$、以及更好懂的替代路徑
freezed `class X with _$X implements Y` 的分層結構解剖:`with` 與 `_$` 各自的角色、沒有 freezed 怎麼手做、中間投影物件 vs DTO 直接 implements 的維護取捨。
- Dart test 的跨檔案 GetX 狀態污染:flaky 真因不是 fail 訊息上的那個 test
`flutter test` 整套跑隨機 fail、單獨跑該 file 卻 100% 過。根因是 dart test runner 同 process 內 GetX state 跨 file 污染,fail 位置看 `+N -1` 累計而非訊息標示的 test。
- Dart StreamController:single-subscription vs broadcast 的設計選型問題
Dart `Bad state: Stream has already been listened to.` 的根因:預設單訂閱在第二個訂閱者出現時才爆。StreamController vs .broadcast() 修復決策、與 Rx / .obs 的比較。
- Gradle JVM target 除錯復盤:七個節點的策略權衡
Gradle JVM target 不一致的除錯決策復盤,重點在每步的策略權衡與走過的彎路。
- Gradle 強制覆寫 plugin 的 JVM target:Kotlin 與 Java 的切入點不對稱
Kotlin / AGP 升級後 build 報 `Inconsistent JVM-target compatibility`。為何要強制覆寫 plugin 的 JVM target,以及 Kotlin 與 Java 設定切入點的不對稱。
- 為什麼 Bug 在合併後才爆:Gradle Cache 掩蓋潛伏問題的邏輯
feature branch build 正常、合併到 main 後才爆、但合併前 main 也沒錯。根因早已潛伏,Gradle cache 掩蓋、合併只是觸發條件。
- Flutter HitTestBehavior:控制點擊命中測試的三種模式
GestureDetector 點空白 padding 區沒反應、或點擊穿透/阻擋行為不符預期。HitTestBehavior 各模式(deferToChild / opaque / translucent)的命中規則與適用場景。
- Freezed 選型評估
Dart 專案是否引入 freezed 的選型評估:原型階段選 json_serializable + Equatable 取代 freezed 的理由與優缺點對照。
- 系統化除錯方法論
警告修復標準化流程
- flutter 可以使用的 togglebutton 樣式
Flutter 切換選項按鈕的元件選型。並列 ToggleButtons 等可用樣式的外觀截圖與程式碼。