論述基礎與限制

本卡的論述基於一次 mobile app 驗收:專案使用 flutter_screenutil(以設計稿座標系等比換算元件尺寸),驗收仍抓到兩個版面事故 — 關鍵計數文字被 flex 寬度競爭壓成「…」、篩選列在螢幕邊緣被截斷。開發端的第一個疑問是「這沒有被 screenutil 判斷出來,是哪邊的問題?」。具體限制:

  • 單一工具的觀察。screenutil 的職責邊界(design px → device px 的數值換算)是這個套件的設計選擇;其他響應式方案(breakpoint 系統、constraint-based layout、CSS 的 clamp/minmax)的保證層各不相同,套用本卡前先確認手上工具實際保證什麼。
  • 本卡談的是「工具保證層與問題發生層的對位」,不談換算層本身的正確性 — 設計稿基準設錯、.w/.h/.sp 用混這類換算層的錯是另一類問題,screenutil 的文件與型別有管到。

核心原則

工具的保證範圍是它的輸入輸出所在的層;問題發生在另一層時,工具的靜默不是「沒問題」、是「沒看」。 等比縮放套件工作在常數層:輸入設計稿座標的固定數字、輸出當前螢幕比例下的固定數字,發生在 layout 之前。而版面擠壓發生在空間分配層:layout 系統拿有限寬度按 flex 規則分給子元件、輸入是動態內容(計數文字長度、chip 數量、i18n 字串、書名)— 換算工具根本不參與這個協商。

兩層各自的失敗模式:

職責失敗形態誰能抓
尺寸換算層design px → device px基準錯、單位用混、字級不隨系統縮放套件文件 / 型別、視覺比對
空間分配層有限空間分給動態內容flex 競爭把內容壓到零、溢出、截斷窄幕驗收、layout / golden test(快照比對測試)

等比縮放還有一個隱含假設:實際內容 = 設計稿內容。設計稿是單一寬度、靜態文案的快照;動態計數、翻譯後的字串長度、使用者資料(書名)、極端螢幕比例都會打破它 — 每個常數都縮放得正確,layout 照樣把縮放正確的元件塞不進縮放正確的容器。比例正確不等於空間夠用。


反模式:把套件當成整層品質的保證

「我們有用 X 套件、為什麼還會出這種問題」— 這句歸因把套件的存在當成了它所在維度(「尺寸」「響應式」)的整層保證。實際上套件只保證它的輸入輸出層,維度裡的其他層照樣裸奔:

  • 裝了等比縮放 → 以為「尺寸問題」整類免檢 → 空間分配層無人看管
  • 對照同族:CI 綠燈被讀成「沒有錯誤」(實際只擋了字面層)、CSS 修完被讀成「呈現沒問題」(實際語意層照舊)

這個 false confidence 比沒裝套件更危險 — 沒裝的時候「版面會不會擠」還在警覺清單上;裝了之後這個問題被歸檔為「套件的事」,從所有人的視野消失,直到驗收或使用者回報。

第二個反模式是歸因方向錯誤:版面擠壓被當成「套件失效」去查套件設定 — 查不出肇因,因為問題從來不在套件的層。診斷起點應該是「這個問題發生在哪一層」,而不是「我裝的工具為什麼沒抓到」。


修法:設計、實作、驗證各補各的

版面擠壓類問題的責任分佈在三層,各有各的產物:

  1. 設計層補「空間競爭規格」。設計稿只回答「長什麼樣」,要另外回答「寬度不夠時誰讓步、怎麼讓」:內容優先序(計數 vs 按鈕誰先縮)、降級策略(縮格式「3/45」/ 換行 / 捲動,ellipsis 對短關鍵資訊是錯誤降級)。沒有這份規格時,優先權由實作層的預設值代答。

  2. 實作層把 flex 組合當成決策Spacer + Flexible + ellipsis 這組慣用寫法本身就是「這段文字可以被犧牲到零」的優先權決策 — 寫的時候要意識到自己在替設計層回答問題,對不上規格就回頭問。

  3. 驗證層補窄幕檢查。能抓到空間分配問題的是窄幕裝置驗收、layout / golden test、overflow 斷言 — 不是 sizing 套件。引入任何「解決某維度」的套件時,順手列出該維度中套件不保證的層、把缺口層掛上對應驗證。

判斷式一句:「這是尺寸換算錯、還是空間分配錯?」 換算錯查套件用法;分配錯改 layout 優先權 + 補上游規格 — 兩條修法路徑完全不同,先分層再動手。


跟其他抽象層原則的關係

原則關係
#92 視覺手段對齊錯誤層次本卡是 #92 家族的第三個 sibling — #82 是「驗證工具 vs 錯誤層次」、#92 是「呈現工具 vs 內容層次」、本卡是「換算工具 vs 佈局層次」,同骨:工具有能擋的層 / 擋不到的層、超出 ceiling 是 false confidence
#82 字面攔截 vs 行為精煉「用了 screenutil = 尺寸沒問題」跟 #82 的「CI 通過 = 沒事」是同型 false confidence — 工具擋字面(常數換算)、抓不到行為(空間協商)
#221 檢查規則的作用域要顯式列舉#221 說零 error 有兩個成因(通過檢查 / 不在作用域)、工具鏈不區分;本卡把同一觀察推到跨層:工具靜默也有兩個成因(該層正確 / 問題不在該層)。#221 是同一層內的涵蓋缺口、本卡是跨層的職責錯位
#56 視覺完成 ≠ 功能完成寬幕開發機上「看起來沒問題」是視覺完成訊號 — 空間分配的失敗只在窄幕 / 長內容下現形,用 #56 的語言:驗收訊號成立的條件要涵蓋失敗才會出現的條件
#11 在開發循環裡早一點用 playwright 看真實結果空間分配是 runtime 協商、靜態讀 code 和設計稿都推不出結果 — 窄幕實機執行是這一層的「早一點看真實結果」

套用到本系統的 case

書庫管理 app 驗收(案例卡 U.C19U.C16):

  • 「已選擇: {count}/{total}」與同列的 flex:2 按鈕、Spacer 按比例分配自由寬度,分到的份額容不下整串、ellipsis 壓成「…」— state 正確、換算正確、回饋死在空間分配。
  • 篩選 chips 超出螢幕寬、截斷處無捲動 affordance — chip 尺寸全部等比正確,總寬 vs 可用寬的關係不在任何工具的視野裡。

兩案的共同點:專案全程使用 flutter_screenutil、兩個問題都不在它的層。開發端的疑問「為什麼套件沒判斷出來」正是本卡反模式段描述的歸因方向錯誤 — 答案是「它從來不負責這層」,而這個答案應該在引入套件時就被寫下來。


判讀徵兆

訊號該做的事
「我們有用 X 套件、為什麼還會出 Y 問題」先問 Y 發生在哪一層、X 保證哪一層 — 對不上就不是套件失效
版面問題去查 sizing 套件的設定歸因方向錯誤候選 — 先分「換算錯 vs 分配錯」
內容是動態的(計數 / i18n / 使用者資料)設計稿的靜態假設已破 — 補空間競爭規格(誰讓步、怎麼降級)
Spacer / Expanded 與承載回饋的 Flexible 同列有人正在用預設值替設計層做優先權決策 — 顯式化
寬幕開發機上一切正常空間分配的失敗條件沒被涵蓋 — 窄幕 / 長內容驗收
引入「解決某維度」的套件列出該維度中套件不保證的層、缺口層掛對應驗證

核心:套件的存在只證明它的層有人管。引入工具時最有價值的一句記錄是「它不保證什麼」— 這句話決定了哪些檢查不能省。