<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>UX 設計案例庫 on Tarragon</title><link>https://tarrragon.github.io/blog/ux-design/cases/</link><description>Recent content in UX 設計案例庫 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/ux-design/cases/index.xml" rel="self" type="application/rss+xml"/><item><title>U.C1 Terminal 畫面五個狀態零個退出路徑</title><link>https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/</guid><description>&lt;p>這個案例的核心責任是說明「每個畫面每個狀態都需要退出路徑」這個原則為什麼容易在企劃階段被遺漏，以及用什麼工具能系統性地捕捉這類缺口。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 的 Terminal 畫面用一個 &lt;code>TerminalScreenUiState&lt;/code> enum 管理五個狀態。實機測試前，五個狀態的 UI 實作如下：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態&lt;/th>
 &lt;th>顯示&lt;/th>
 &lt;th>可用操作&lt;/th>
 &lt;th>退出路徑&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>idle&lt;/td>
 &lt;td>空白（自動連線）&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connecting&lt;/td>
 &lt;td>進度指示&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connected&lt;/td>
 &lt;td>終端機 + 工具列&lt;/td>
 &lt;td>打字、特殊鍵&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error&lt;/td>
 &lt;td>錯誤訊息 + 重連按鈕&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>disconnected&lt;/td>
 &lt;td>「連線中斷」+ 重連按鈕&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>使用者從首頁點 Connect Terminal 進入後，無論處於哪個狀態都無法返回首頁。唯一退出方式是殺掉 app。&lt;/p>
&lt;p>W2-001 修復後加入 back 按鈕的狀態：error、disconnected、connecting。但 idle 和 connected 仍缺退出路徑。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>企劃文件的「前端引導」欄位只描述顯示，不描述操作和退出&lt;/strong>。操作盤點表的「前端引導」欄位寫了「連線失敗顯示無法連線」— 覆蓋了 error 狀態的顯示，但沒回答「能做什麼」和「怎麼離開」。從 BDD 操作盤點到 UI 實作之間，缺少把「情境」展開成「畫面 × 狀態 × 操作 × 退出」矩陣的步驟。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>開發者假設使用者只走 happy path&lt;/strong>。「connected 後使用者不會想回首頁」是開發者的隱性假設。實際上使用者可能想：切換到配對畫面重新配對、暫時離開終端機做其他事、遇到問題想重新開始。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>error 和 disconnected 有重連按鈕但沒有 back，也是半成品&lt;/strong>。重連失敗時使用者被困在 error → retry → error 的循環裡。加 back 按鈕讓使用者有第二條路。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>畫面狀態矩陣作為設計產物&lt;/strong>：把每個畫面的每個狀態展開成四欄表格（顯示 / 可用操作 / 進入條件 / 退出路徑）。退出路徑欄位為空 = UX 死胡同，10 分鐘能查完所有畫面。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>退出路徑是預設要求&lt;/strong>：每個畫面的每個狀態至少要有一條退出路徑。即使是 connecting 這種過渡狀態，使用者也應該能取消。這跟 iOS HIG 和 Material Design 對 modal 畫面的 dismiss 要求一致。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Widget test 覆蓋退出路徑&lt;/strong>：狀態矩陣直接轉成 test case — 每個狀態找到 back 按鈕、tap、斷言導航到首頁。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想用狀態矩陣設計畫面 → &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法&lt;/a>&lt;/li>
&lt;li>想建 widget test 覆蓋導航 → &lt;a href="https://tarrragon.github.io/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">模組四：自動化 UI 驗證&lt;/a>&lt;/li>
&lt;li>類似案例（Gate fallback）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2 biometricOnly 無 fallback&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「每個畫面每個狀態都需要退出路徑」這個原則為什麼容易在企劃階段被遺漏，以及用什麼工具能系統性地捕捉這類缺口。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 的 Terminal 畫面用一個 <code>TerminalScreenUiState</code> enum 管理五個狀態。實機測試前，五個狀態的 UI 實作如下：</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>顯示</th>
          <th>可用操作</th>
          <th>退出路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>idle</td>
          <td>空白（自動連線）</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>connecting</td>
          <td>進度指示</td>
          <td>無</td>
          <td>無</td>
      </tr>
      <tr>
          <td>connected</td>
          <td>終端機 + 工具列</td>
          <td>打字、特殊鍵</td>
          <td>無</td>
      </tr>
      <tr>
          <td>error</td>
          <td>錯誤訊息 + 重連按鈕</td>
          <td>重新連線</td>
          <td>無</td>
      </tr>
      <tr>
          <td>disconnected</td>
          <td>「連線中斷」+ 重連按鈕</td>
          <td>重新連線</td>
          <td>無</td>
      </tr>
  </tbody>
</table>
<p>使用者從首頁點 Connect Terminal 進入後，無論處於哪個狀態都無法返回首頁。唯一退出方式是殺掉 app。</p>
<p>W2-001 修復後加入 back 按鈕的狀態：error、disconnected、connecting。但 idle 和 connected 仍缺退出路徑。</p>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>企劃文件的「前端引導」欄位只描述顯示，不描述操作和退出</strong>。操作盤點表的「前端引導」欄位寫了「連線失敗顯示無法連線」— 覆蓋了 error 狀態的顯示，但沒回答「能做什麼」和「怎麼離開」。從 BDD 操作盤點到 UI 實作之間，缺少把「情境」展開成「畫面 × 狀態 × 操作 × 退出」矩陣的步驟。</p>
</li>
<li>
<p><strong>開發者假設使用者只走 happy path</strong>。「connected 後使用者不會想回首頁」是開發者的隱性假設。實際上使用者可能想：切換到配對畫面重新配對、暫時離開終端機做其他事、遇到問題想重新開始。</p>
</li>
<li>
<p><strong>error 和 disconnected 有重連按鈕但沒有 back，也是半成品</strong>。重連失敗時使用者被困在 error → retry → error 的循環裡。加 back 按鈕讓使用者有第二條路。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li>
<p><strong>畫面狀態矩陣作為設計產物</strong>：把每個畫面的每個狀態展開成四欄表格（顯示 / 可用操作 / 進入條件 / 退出路徑）。退出路徑欄位為空 = UX 死胡同，10 分鐘能查完所有畫面。</p>
</li>
<li>
<p><strong>退出路徑是預設要求</strong>：每個畫面的每個狀態至少要有一條退出路徑。即使是 connecting 這種過渡狀態，使用者也應該能取消。這跟 iOS HIG 和 Material Design 對 modal 畫面的 dismiss 要求一致。</p>
</li>
<li>
<p><strong>Widget test 覆蓋退出路徑</strong>：狀態矩陣直接轉成 test case — 每個狀態找到 back 按鈕、tap、斷言導航到首頁。</p>
</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想用狀態矩陣設計畫面 → <a href="/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法</a></li>
<li>想建 widget test 覆蓋導航 → <a href="/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">模組四：自動化 UI 驗證</a></li>
<li>類似案例（Gate fallback）→ <a href="/blog/ux-design/cases/biometric-only-no-fallback/" data-link-title="U.C2 biometricOnly=true 無密碼 fallback" data-link-desc="Flutter app 的生物辨識設定 biometricOnly: true 阻擋所有非生物辨識認證方式 — Face ID 不可用時使用者直接被擋住，沒有替代路徑">U.C2 biometricOnly 無 fallback</a></li>
</ul>
]]></content:encoded></item><item><title>U.C2 biometricOnly=true 無密碼 fallback</title><link>https://tarrragon.github.io/blog/ux-design/cases/biometric-only-no-fallback/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/biometric-only-no-fallback/</guid><description>&lt;p>這個案例的核心責任是說明 Gate（使用者必須通過的關卡）的設計不只是「成功時怎麼做」，還必須包含「失敗時的替代路徑」。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 使用 &lt;code>local_auth&lt;/code> 套件進行生物辨識認證。&lt;code>AuthenticationOptions&lt;/code> 設定 &lt;code>biometricOnly: true&lt;/code>，表示只接受生物辨識（Face ID / 指紋），不接受裝置密碼作為 fallback。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 修復前
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nl">options:&lt;/span> &lt;span class="kd">const&lt;/span> &lt;span class="n">AuthenticationOptions&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nl">stickyAuth:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nl">biometricOnly:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Face ID 不可用 → 認證直接失敗
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">// 修復後
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nl">options:&lt;/span> &lt;span class="kd">const&lt;/span> &lt;span class="n">AuthenticationOptions&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nl">stickyAuth:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nl">biometricOnly:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Face ID 不可用 → 系統自動提示輸入裝置密碼
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">),&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>影響範圍&lt;/td>
 &lt;td>Face ID 不可用時（戴口罩、光線差、指紋模糊、模擬器）完全無法使用 app&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復成本&lt;/td>
 &lt;td>改一個 boolean&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>根因&lt;/td>
 &lt;td>企劃階段未設計 biometric gate 的 fallback&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Gate fallback 是設計問題，不是實作問題&lt;/strong>。&lt;code>biometricOnly&lt;/code> 的預設值是 &lt;code>false&lt;/code>（允許密碼 fallback），開發時特意改成 &lt;code>true&lt;/code> 是因為認為「安全性更高」。但這個判斷沒有考慮 fallback 缺失時的 UX 代價 — 使用者完全無法進入 app。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>開發環境遮蔽了問題&lt;/strong>。iOS 模擬器預設不支援 Face ID，但 &lt;code>isAvailable()&lt;/code> 的實作會檢查 &lt;code>isDeviceSupported()&lt;/code> + &lt;code>getAvailableBiometrics().isNotEmpty&lt;/code>。模擬器回傳 &lt;code>isDeviceSupported() = true&lt;/code> 但 &lt;code>getAvailableBiometrics() = []&lt;/code>，所以在模擬器上 &lt;code>isAvailable()&lt;/code> 回傳 false，直接跳過認證走預設路徑。真實裝置上 &lt;code>isAvailable() = true&lt;/code> 但 Face ID 可能失敗，這時沒有 fallback。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>安全性 vs 可用性的取捨需要顯式記錄&lt;/strong>。&lt;code>biometricOnly: true&lt;/code> 的安全收益是「確保只有生物特徵擁有者能操作」；代價是「任何生物辨識失敗場景都阻擋使用」。自用工具的使用者就是 owner，密碼 fallback 的安全風險遠低於「完全無法使用」的可用性風險。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>每個 gate 設計時列三問&lt;/strong>：成功時做什麼？失敗時做什麼？使用者不知道發生什麼時做什麼？&lt;/li>
&lt;li>&lt;strong>在狀態矩陣標注 gate fallback&lt;/strong>：biometric / network / auth 每個 gate 旁邊標注替代路徑，空白 = 使用者被擋住。&lt;/li>
&lt;li>&lt;strong>安全 vs 可用性取捨顯式記錄&lt;/strong>：在 spec 文件記錄「&lt;code>biometricOnly: false&lt;/code> — 接受密碼 fallback，因為自用工具可用性優先於生物辨識強制」。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想設計 Gate fallback 體系 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法&lt;/a>&lt;/li>
&lt;li>想了解 biometric 在不同平台的行為差異 → 待補：iOS/Android biometric API 行為對照&lt;/li>
&lt;li>類似案例（導航死胡同）→ &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1 五個狀態零個退出&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 Gate（使用者必須通過的關卡）的設計不只是「成功時怎麼做」，還必須包含「失敗時的替代路徑」。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 使用 <code>local_auth</code> 套件進行生物辨識認證。<code>AuthenticationOptions</code> 設定 <code>biometricOnly: true</code>，表示只接受生物辨識（Face ID / 指紋），不接受裝置密碼作為 fallback。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 修復前
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nl">options:</span> <span class="kd">const</span> <span class="n">AuthenticationOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nl">stickyAuth:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nl">biometricOnly:</span> <span class="kc">true</span><span class="p">,</span>  <span class="c1">// Face ID 不可用 → 認證直接失敗
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// 修復後
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="nl">options:</span> <span class="kd">const</span> <span class="n">AuthenticationOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nl">stickyAuth:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nl">biometricOnly:</span> <span class="kc">false</span><span class="p">,</span> <span class="c1">// Face ID 不可用 → 系統自動提示輸入裝置密碼
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="p">),</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>指標</th>
          <th>值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>影響範圍</td>
          <td>Face ID 不可用時（戴口罩、光線差、指紋模糊、模擬器）完全無法使用 app</td>
      </tr>
      <tr>
          <td>修復成本</td>
          <td>改一個 boolean</td>
      </tr>
      <tr>
          <td>根因</td>
          <td>企劃階段未設計 biometric gate 的 fallback</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>Gate fallback 是設計問題，不是實作問題</strong>。<code>biometricOnly</code> 的預設值是 <code>false</code>（允許密碼 fallback），開發時特意改成 <code>true</code> 是因為認為「安全性更高」。但這個判斷沒有考慮 fallback 缺失時的 UX 代價 — 使用者完全無法進入 app。</p>
</li>
<li>
<p><strong>開發環境遮蔽了問題</strong>。iOS 模擬器預設不支援 Face ID，但 <code>isAvailable()</code> 的實作會檢查 <code>isDeviceSupported()</code> + <code>getAvailableBiometrics().isNotEmpty</code>。模擬器回傳 <code>isDeviceSupported() = true</code> 但 <code>getAvailableBiometrics() = []</code>，所以在模擬器上 <code>isAvailable()</code> 回傳 false，直接跳過認證走預設路徑。真實裝置上 <code>isAvailable() = true</code> 但 Face ID 可能失敗，這時沒有 fallback。</p>
</li>
<li>
<p><strong>安全性 vs 可用性的取捨需要顯式記錄</strong>。<code>biometricOnly: true</code> 的安全收益是「確保只有生物特徵擁有者能操作」；代價是「任何生物辨識失敗場景都阻擋使用」。自用工具的使用者就是 owner，密碼 fallback 的安全風險遠低於「完全無法使用」的可用性風險。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li><strong>每個 gate 設計時列三問</strong>：成功時做什麼？失敗時做什麼？使用者不知道發生什麼時做什麼？</li>
<li><strong>在狀態矩陣標注 gate fallback</strong>：biometric / network / auth 每個 gate 旁邊標注替代路徑，空白 = 使用者被擋住。</li>
<li><strong>安全 vs 可用性取捨顯式記錄</strong>：在 spec 文件記錄「<code>biometricOnly: false</code> — 接受密碼 fallback，因為自用工具可用性優先於生物辨識強制」。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 Gate fallback 體系 → <a href="/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法</a></li>
<li>想了解 biometric 在不同平台的行為差異 → 待補：iOS/Android biometric API 行為對照</li>
<li>類似案例（導航死胡同）→ <a href="/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1 五個狀態零個退出</a></li>
</ul>
]]></content:encoded></item><item><title>U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField</title><link>https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/</guid><description>&lt;p>這個案例的核心責任是說明輸入機制是設計產物（在企劃階段決定），不是實作細節（在寫 code 時順便加）。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 的 Terminal 畫面在 W2 修復前沒有任何文字輸入元件。使用者只能透過底部工具列的特殊鍵（Esc/Tab/Ctrl/方向鍵）操作終端機，無法打字。&lt;/p>
&lt;p>W2-001 修復時加入的 &lt;code>TextField&lt;/code> 及其參數：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">TextField&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nl">keyboardType:&lt;/span> &lt;span class="n">TextInputType&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">visiblePassword&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 避免自動校正
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">enableSuggestions:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 關閉建議列
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">autocorrect:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 關閉自動校正
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">enableIMEPersonalizedLearning:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 關閉 IME 個人化學習
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">onSubmitted:&lt;/span> &lt;span class="n">_submitInput&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Enter 送出整行
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">textInputAction:&lt;/span> &lt;span class="n">TextInputAction&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">send&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 鍵盤顯示「傳送」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個參數都是一個設計決策，但沒有一個是事前規劃的 — 全部是寫 code 時臨時判斷。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>設計決策&lt;/th>
 &lt;th>事前規劃&lt;/th>
 &lt;th>事後 hotfix 的風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>visiblePassword&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果用預設 &lt;code>text&lt;/code>，iOS 會自動校正 &lt;code>ls -la&lt;/code> 成其他東西&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>enableSuggestions: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>建議列遮擋終端機畫面下方&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>autocorrect: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>路徑 &lt;code>/usr/bin/&lt;/code> 可能被校正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>enableIMEPersonalizedLearning: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>CLI 輸入含密碼和路徑，IME 學習是安全風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>onSubmitted&lt;/code>（整行送出）&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果逐字元送出，Tab 補全和命令編輯需要完全不同的 protocol 設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>TextInputAction.send&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果用 &lt;code>newline&lt;/code>，使用者按 Enter 會換行不送出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>輸入設計影響 UI layout 和 protocol&lt;/strong>。&lt;code>onSubmitted&lt;/code>（整行送出）vs 逐字元即時送出不只是 UI 問題 — 整行送出代表 protocol 層送的是 &lt;code>command\n&lt;/code>，逐字元送出代表每個按鍵都是一個 WS frame。這個決策應該在 protocol spec 階段就做，因為它影響 server 端的行為預期。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>IME 控制有安全意涵&lt;/strong>。&lt;code>enableIMEPersonalizedLearning: false&lt;/code> 不只是 UX 偏好 — CLI 輸入可能包含資料庫密碼、API key、伺服器路徑。IME 學習這些內容等於把 secret 存到了 IME 的詞庫裡，跨 app 可用。這是安全問題，不是 UX 問題。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>事後 hotfix 的六個參數每個都有 gotcha&lt;/strong>。如果這些決策在企劃階段做，可以寫成決策表並在 code review 時對照。事後 hotfix 時開發者可能漏掉其中一兩個（例如只加 &lt;code>autocorrect: false&lt;/code> 但忘了 &lt;code>enableIMEPersonalizedLearning: false&lt;/code>），漏掉的那個就成為安全漏洞。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>功能規格新增「輸入機制決策表」&lt;/strong>：keyboard type / submit model / IME policy / special keys 四個維度，每個列出選項和取捨理由。&lt;/li>
&lt;li>&lt;strong>輸入機制跟 protocol 一起設計&lt;/strong>：「整行送出」還是「逐字元」決定了 WS 訊框的設計，必須在 protocol spec 階段決定。&lt;/li>
&lt;li>&lt;strong>安全敏感參數強制列入 review checklist&lt;/strong>：&lt;code>enableIMEPersonalizedLearning&lt;/code>、&lt;code>autocorrect&lt;/code> 在處理 secret 的輸入框中是安全要求，不是可選項。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想設計 mobile 輸入機制 → &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表&lt;/a>&lt;/li>
&lt;li>想看 protocol 跟輸入的關聯 → &lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type&lt;/a>（sendData 的型別決策）&lt;/li>
&lt;li>想做安全審查 → 待補：CLI 輸入安全 checklist&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明輸入機制是設計產物（在企劃階段決定），不是實作細節（在寫 code 時順便加）。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 的 Terminal 畫面在 W2 修復前沒有任何文字輸入元件。使用者只能透過底部工具列的特殊鍵（Esc/Tab/Ctrl/方向鍵）操作終端機，無法打字。</p>
<p>W2-001 修復時加入的 <code>TextField</code> 及其參數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">TextField</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nl">keyboardType:</span> <span class="n">TextInputType</span><span class="p">.</span><span class="n">visiblePassword</span><span class="p">,</span>   <span class="c1">// 避免自動校正
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nl">enableSuggestions:</span> <span class="kc">false</span><span class="p">,</span>                       <span class="c1">// 關閉建議列
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nl">autocorrect:</span> <span class="kc">false</span><span class="p">,</span>                             <span class="c1">// 關閉自動校正
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nl">enableIMEPersonalizedLearning:</span> <span class="kc">false</span><span class="p">,</span>           <span class="c1">// 關閉 IME 個人化學習
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nl">onSubmitted:</span> <span class="n">_submitInput</span><span class="p">,</span>                      <span class="c1">// Enter 送出整行
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="nl">textInputAction:</span> <span class="n">TextInputAction</span><span class="p">.</span><span class="n">send</span><span class="p">,</span>          <span class="c1">// 鍵盤顯示「傳送」
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="p">)</span></span></span></code></pre></div><p>每個參數都是一個設計決策，但沒有一個是事前規劃的 — 全部是寫 code 時臨時判斷。</p>
<table>
  <thead>
      <tr>
          <th>設計決策</th>
          <th>事前規劃</th>
          <th>事後 hotfix 的風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>visiblePassword</code></td>
          <td>沒有</td>
          <td>如果用預設 <code>text</code>，iOS 會自動校正 <code>ls -la</code> 成其他東西</td>
      </tr>
      <tr>
          <td><code>enableSuggestions: false</code></td>
          <td>沒有</td>
          <td>建議列遮擋終端機畫面下方</td>
      </tr>
      <tr>
          <td><code>autocorrect: false</code></td>
          <td>沒有</td>
          <td>路徑 <code>/usr/bin/</code> 可能被校正</td>
      </tr>
      <tr>
          <td><code>enableIMEPersonalizedLearning: false</code></td>
          <td>沒有</td>
          <td>CLI 輸入含密碼和路徑，IME 學習是安全風險</td>
      </tr>
      <tr>
          <td><code>onSubmitted</code>（整行送出）</td>
          <td>沒有</td>
          <td>如果逐字元送出，Tab 補全和命令編輯需要完全不同的 protocol 設計</td>
      </tr>
      <tr>
          <td><code>TextInputAction.send</code></td>
          <td>沒有</td>
          <td>如果用 <code>newline</code>，使用者按 Enter 會換行不送出</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>輸入設計影響 UI layout 和 protocol</strong>。<code>onSubmitted</code>（整行送出）vs 逐字元即時送出不只是 UI 問題 — 整行送出代表 protocol 層送的是 <code>command\n</code>，逐字元送出代表每個按鍵都是一個 WS frame。這個決策應該在 protocol spec 階段就做，因為它影響 server 端的行為預期。</p>
</li>
<li>
<p><strong>IME 控制有安全意涵</strong>。<code>enableIMEPersonalizedLearning: false</code> 不只是 UX 偏好 — CLI 輸入可能包含資料庫密碼、API key、伺服器路徑。IME 學習這些內容等於把 secret 存到了 IME 的詞庫裡，跨 app 可用。這是安全問題，不是 UX 問題。</p>
</li>
<li>
<p><strong>事後 hotfix 的六個參數每個都有 gotcha</strong>。如果這些決策在企劃階段做，可以寫成決策表並在 code review 時對照。事後 hotfix 時開發者可能漏掉其中一兩個（例如只加 <code>autocorrect: false</code> 但忘了 <code>enableIMEPersonalizedLearning: false</code>），漏掉的那個就成為安全漏洞。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li><strong>功能規格新增「輸入機制決策表」</strong>：keyboard type / submit model / IME policy / special keys 四個維度，每個列出選項和取捨理由。</li>
<li><strong>輸入機制跟 protocol 一起設計</strong>：「整行送出」還是「逐字元」決定了 WS 訊框的設計，必須在 protocol spec 階段決定。</li>
<li><strong>安全敏感參數強制列入 review checklist</strong>：<code>enableIMEPersonalizedLearning</code>、<code>autocorrect</code> 在處理 secret 的輸入框中是安全要求，不是可選項。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 mobile 輸入機制 → <a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a></li>
<li>想看 protocol 跟輸入的關聯 → <a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type</a>（sendData 的型別決策）</li>
<li>想做安全審查 → 待補：CLI 輸入安全 checklist</li>
</ul>
]]></content:encoded></item><item><title>U.C4 首頁缺配對入口按鈕、導航流未完整列出</title><link>https://tarrragon.github.io/blog/ux-design/cases/missing-enrollment-entry-point/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/missing-enrollment-entry-point/</guid><description>&lt;p>這個案例的核心責任是說明導航流設計必須覆蓋所有操作情境的入口，不只是最常用的那個。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 首頁在 W2-001 修復前只有一個按鈕：Connect Terminal（對應 UC-02 日常連線）。配對功能（UC-01 首次配對）沒有入口 — &lt;code>EnrollmentScreen&lt;/code> 和 &lt;code>QrScannerScreen&lt;/code> 都存在且可運作，但首頁沒有按鈕導航過去。&lt;/p>
&lt;p>Router 定義了三條路由，全部可存取：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">GoRouter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">routes:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="n">GoRoute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">path:&lt;/span> &lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nl">builder:&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="n">HomeScreen&lt;/span>&lt;span class="p">()),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">GoRoute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">path:&lt;/span> &lt;span class="s1">&amp;#39;/enrollment&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nl">builder:&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="n">EnrollmentScreen&lt;/span>&lt;span class="p">()),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="n">GoRoute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">path:&lt;/span> &lt;span class="s1">&amp;#39;/terminal&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nl">builder:&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="n">TerminalScreen&lt;/span>&lt;span class="p">()),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">]);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但 HomeScreen 只有一個 &lt;code>context.go('/terminal')&lt;/code> 按鈕，&lt;code>/enrollment&lt;/code> 路由存在但從 UI 無法到達。&lt;/p>
&lt;p>W2-001 修復加入 &lt;code>OutlinedButton.icon&lt;/code> 連結到 &lt;code>/enrollment&lt;/code>，並用 &lt;code>context.push&lt;/code>（非 &lt;code>context.go&lt;/code>）讓配對完成後能返回首頁。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>影響&lt;/td>
 &lt;td>首次使用者無法配對（功能存在但入口缺失）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復&lt;/td>
 &lt;td>加一個 &lt;code>OutlinedButton&lt;/code> + &lt;code>context.push('/enrollment')&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>根因&lt;/td>
 &lt;td>導航流只設計了「日常連線」入口，遺漏「首次配對」入口&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>操作盤點有四個操作，首頁只有一個入口&lt;/strong>。操作盤點段列出四個操作：配對、連線、輪替、啟停。首頁應該是這四個操作的導航 hub，至少要有「配對」和「連線」兩個入口（輪替和啟停是主機端操作，不需要 app 入口）。只放 Connect Terminal 等於假設「使用者已經配對過」。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>路由存在但 UI 不可達 = 死程式碼的 UX 版本&lt;/strong>。&lt;code>/enrollment&lt;/code> 路由在 router 裡定義了，&lt;code>EnrollmentScreen&lt;/code> 也完整實作了，但使用者從 UI 無法觸及。這跟寫了函式但沒有呼叫者一樣 — 功能正確但不可存取。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>&lt;code>go&lt;/code> vs &lt;code>push&lt;/code> 的語意差異影響 UX&lt;/strong>。W2 修復用 &lt;code>context.push('/enrollment')&lt;/code> 而非 &lt;code>context.go('/enrollment')&lt;/code> — &lt;code>push&lt;/code> 保留返回堆疊讓使用者配對後按 back 回首頁；&lt;code>go&lt;/code> 替換整個路由堆疊、沒有 back。這個決策影響使用者的導航體驗，但也是事後才想到的。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>導航流從操作盤點反推&lt;/strong>：每個 UC（用例）的主入口在哪？首頁應該是哪些 UC 的 hub？列出來，確認每個 UC 至少有一條從首頁可達的路徑。&lt;/li>
&lt;li>&lt;strong>路由可達性檢查&lt;/strong>：router 定義的每個路由都應該從 UI 可達。不可達的路由要嘛是遺漏入口（本案例），要嘛是應該刪除的死路由。可以寫一個 lint 檢查。&lt;/li>
&lt;li>&lt;strong>首次 vs 日常使用者的 UX 區分&lt;/strong>：首次使用者需要 onboarding 流程（配對 → 連線），日常使用者只需要連線。兩種入口都要在首頁可見，但可以用視覺層級區分主要/次要。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想設計完整導航流 → &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/" data-link-title="模組五：導航模式" data-link-desc="Push/pop stack、GoRouter 命名路由、tab bar、drawer — 導航方法選擇是設計決策">模組五：導航模式&lt;/a>&lt;/li>
&lt;li>想檢查畫面狀態矩陣的退出路徑 → &lt;a href="https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1 五狀態零退出&lt;/a>&lt;/li>
&lt;li>想做路由可達性自動化檢查 → 待補：Flutter GoRouter 路由可達性 lint&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明導航流設計必須覆蓋所有操作情境的入口，不只是最常用的那個。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 首頁在 W2-001 修復前只有一個按鈕：Connect Terminal（對應 UC-02 日常連線）。配對功能（UC-01 首次配對）沒有入口 — <code>EnrollmentScreen</code> 和 <code>QrScannerScreen</code> 都存在且可運作，但首頁沒有按鈕導航過去。</p>
<p>Router 定義了三條路由，全部可存取：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">GoRouter</span><span class="p">(</span><span class="nl">routes:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">GoRoute</span><span class="p">(</span><span class="nl">path:</span> <span class="s1">&#39;/&#39;</span><span class="p">,</span> <span class="nl">builder:</span> <span class="p">...</span> <span class="n">HomeScreen</span><span class="p">()),</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="n">GoRoute</span><span class="p">(</span><span class="nl">path:</span> <span class="s1">&#39;/enrollment&#39;</span><span class="p">,</span> <span class="nl">builder:</span> <span class="p">...</span> <span class="n">EnrollmentScreen</span><span class="p">()),</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="n">GoRoute</span><span class="p">(</span><span class="nl">path:</span> <span class="s1">&#39;/terminal&#39;</span><span class="p">,</span> <span class="nl">builder:</span> <span class="p">...</span> <span class="n">TerminalScreen</span><span class="p">()),</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">]);</span></span></span></code></pre></div><p>但 HomeScreen 只有一個 <code>context.go('/terminal')</code> 按鈕，<code>/enrollment</code> 路由存在但從 UI 無法到達。</p>
<p>W2-001 修復加入 <code>OutlinedButton.icon</code> 連結到 <code>/enrollment</code>，並用 <code>context.push</code>（非 <code>context.go</code>）讓配對完成後能返回首頁。</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>影響</td>
          <td>首次使用者無法配對（功能存在但入口缺失）</td>
      </tr>
      <tr>
          <td>修復</td>
          <td>加一個 <code>OutlinedButton</code> + <code>context.push('/enrollment')</code></td>
      </tr>
      <tr>
          <td>根因</td>
          <td>導航流只設計了「日常連線」入口，遺漏「首次配對」入口</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>操作盤點有四個操作，首頁只有一個入口</strong>。操作盤點段列出四個操作：配對、連線、輪替、啟停。首頁應該是這四個操作的導航 hub，至少要有「配對」和「連線」兩個入口（輪替和啟停是主機端操作，不需要 app 入口）。只放 Connect Terminal 等於假設「使用者已經配對過」。</p>
</li>
<li>
<p><strong>路由存在但 UI 不可達 = 死程式碼的 UX 版本</strong>。<code>/enrollment</code> 路由在 router 裡定義了，<code>EnrollmentScreen</code> 也完整實作了，但使用者從 UI 無法觸及。這跟寫了函式但沒有呼叫者一樣 — 功能正確但不可存取。</p>
</li>
<li>
<p><strong><code>go</code> vs <code>push</code> 的語意差異影響 UX</strong>。W2 修復用 <code>context.push('/enrollment')</code> 而非 <code>context.go('/enrollment')</code> — <code>push</code> 保留返回堆疊讓使用者配對後按 back 回首頁；<code>go</code> 替換整個路由堆疊、沒有 back。這個決策影響使用者的導航體驗，但也是事後才想到的。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li><strong>導航流從操作盤點反推</strong>：每個 UC（用例）的主入口在哪？首頁應該是哪些 UC 的 hub？列出來，確認每個 UC 至少有一條從首頁可達的路徑。</li>
<li><strong>路由可達性檢查</strong>：router 定義的每個路由都應該從 UI 可達。不可達的路由要嘛是遺漏入口（本案例），要嘛是應該刪除的死路由。可以寫一個 lint 檢查。</li>
<li><strong>首次 vs 日常使用者的 UX 區分</strong>：首次使用者需要 onboarding 流程（配對 → 連線），日常使用者只需要連線。兩種入口都要在首頁可見，但可以用視覺層級區分主要/次要。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計完整導航流 → <a href="/blog/ux-design/05-navigation-patterns/" data-link-title="模組五：導航模式" data-link-desc="Push/pop stack、GoRouter 命名路由、tab bar、drawer — 導航方法選擇是設計決策">模組五：導航模式</a></li>
<li>想檢查畫面狀態矩陣的退出路徑 → <a href="/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1 五狀態零退出</a></li>
<li>想做路由可達性自動化檢查 → 待補：Flutter GoRouter 路由可達性 lint</li>
</ul>
]]></content:encoded></item></channel></rss>