<?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>ViewModel on Tarragon</title><link>https://tarrragon.github.io/blog/tags/viewmodel/</link><description>Recent content in ViewModel on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 04 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/viewmodel/index.xml" rel="self" type="application/rss+xml"/><item><title>分層 i18n 管理方法論 - 業務層國際化的正確實踐</title><link>https://tarrragon.github.io/blog/record/%E5%88%86%E5%B1%A4-i18n-%E7%AE%A1%E7%90%86%E6%96%B9%E6%B3%95%E8%AB%96-%E6%A5%AD%E5%8B%99%E5%B1%A4%E5%9C%8B%E9%9A%9B%E5%8C%96%E7%9A%84%E6%AD%A3%E7%A2%BA%E5%AF%A6%E8%B8%90/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E5%88%86%E5%B1%A4-i18n-%E7%AE%A1%E7%90%86%E6%96%B9%E6%B3%95%E8%AB%96-%E6%A5%AD%E5%8B%99%E5%B1%A4%E5%9C%8B%E9%9A%9B%E5%8C%96%E7%9A%84%E6%AD%A3%E7%A2%BA%E5%AF%A6%E8%B8%90/</guid><description>&lt;p>在開發書庫管理 Flutter 應用的過程中，我們陷入了一個多語言專案幾乎必經的事故：i18n 訊息出現在不該出現的地方。Domain 層的 repository 開始直接拋出中文錯誤訊息，ViewModel 裡散落著硬編碼字串，UI 層還要自己判斷 errorCode 決定顯示什麼文字。每增加一種語言，就要在三個地方同時動手。&lt;/p></description><content:encoded><![CDATA[<p>在開發書庫管理 Flutter 應用的過程中，我們陷入了一個多語言專案幾乎必經的事故：i18n 訊息出現在不該出現的地方。Domain 層的 repository 開始直接拋出中文錯誤訊息，ViewModel 裡散落著硬編碼字串，UI 層還要自己判斷 errorCode 決定顯示什麼文字。每增加一種語言，就要在三個地方同時動手。</p>
<h2 id="問題的根源">問題的根源</h2>
<p>幾個典型的失控情境：</p>
<p>Domain 層的 Service 直接回傳中文字串作為錯誤訊息，要支援英文時就必須深入業務邏輯層去改。ViewModel 裡出現 <code>errorMessage: '找不到書籍'</code> 這樣的程式碼，看起來無害，但需要國際化時根本不知道有多少個地方要改。UI 層開始承擔它不應該承擔的邏輯，自行根據 errorCode 決定顯示什麼文字。</p>
<p>問題的共同根源：沒有清楚定義每一層對 i18n 的責任邊界。</p>
<h2 id="核心原則">核心原則</h2>
<p>一個簡單的原則統治一切：<strong>Domain 層不知道 UI 呈現方式，UI 層不承擔訊息決策邏輯</strong>。</p>
<p>具體化成三條規則：Domain 層使用技術語言（錯誤碼），ViewModel 層負責轉換為使用者語言，所有使用者可見的字串禁止硬編碼。</p>
<h2 id="domain-層回傳錯誤碼不回傳訊息">Domain 層：回傳錯誤碼，不回傳訊息</h2>
<p>Domain 層用 <code>ErrorCode</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">enum</span> <span class="n">BookErrorCode</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="n">notFound</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">invalidIsbn</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="n">networkTimeout</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="n">serverError</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="kd">class</span> <span class="nc">BookRepository</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="n">Future</span><span class="o">&lt;</span><span class="n">Result</span><span class="o">&lt;</span><span class="n">Book</span><span class="p">,</span> <span class="n">BookErrorCode</span><span class="o">&gt;&gt;</span> <span class="n">fetchBook</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">)</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="kd">final</span> <span class="n">response</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">api</span><span class="p">.</span><span class="n">getBook</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">      <span class="k">return</span> <span class="n">Result</span><span class="p">.</span><span class="n">success</span><span class="p">(</span><span class="n">Book</span><span class="p">.</span><span class="n">fromJson</span><span class="p">(</span><span class="n">response</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">}</span> <span class="n">on</span> <span class="n">NotFoundException</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">      <span class="k">return</span> <span class="n">Result</span><span class="p">.</span><span class="n">failure</span><span class="p">(</span><span class="n">BookErrorCode</span><span class="p">.</span><span class="n">notFound</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span> <span class="n">on</span> <span class="n">TimeoutException</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="k">return</span> <span class="n">Result</span><span class="p">.</span><span class="n">failure</span><span class="p">(</span><span class="n">BookErrorCode</span><span class="p">.</span><span class="n">networkTimeout</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不可違反的規則：不引入任何 i18n 相關的 import，不依賴 <code>BuildContext</code>，不出現任何使用者可見的字串。</p>
<h2 id="viewmodel-層轉換錯誤碼為使用者訊息">ViewModel 層：轉換錯誤碼為使用者訊息</h2>
<p>ViewModel 是整個 i18n 流程的轉換點。三個合法的訊息來源：i18n 系統（靜態訊息）、ErrorHandler 轉換器（集中管理錯誤碼對應）、Domain 層直接回傳的動態訊息。</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="kd">class</span> <span class="nc">BookDetailViewModel</span> <span class="kd">extends</span> <span class="n">Notifier</span><span class="o">&lt;</span><span class="n">BookDetailState</span><span class="o">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">BookDetailState</span> <span class="n">build</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">BookDetailState</span><span class="p">.</span><span class="n">initial</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span> <span class="n">loadBook</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">)</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">state</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">copyWith</span><span class="p">(</span><span class="nl">isLoading:</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">bookRepositoryProvider</span><span class="p">).</span><span class="n">fetchBook</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">result</span><span class="p">.</span><span class="n">when</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="nl">success:</span> <span class="p">(</span><span class="n">book</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">state</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">copyWith</span><span class="p">(</span><span class="nl">isLoading:</span> <span class="kc">false</span><span class="p">,</span> <span class="nl">book:</span> <span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">      <span class="nl">failure:</span> <span class="p">(</span><span class="n">errorCode</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="kd">final</span> <span class="n">l10n</span> <span class="o">=</span> <span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">localizationsProvider</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="kd">final</span> <span class="n">message</span> <span class="o">=</span> <span class="n">ErrorHandler</span><span class="p">.</span><span class="n">getMessage</span><span class="p">(</span><span class="n">errorCode</span><span class="p">,</span> <span class="nl">l10n:</span> <span class="n">l10n</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">state</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">copyWith</span><span class="p">(</span><span class="nl">isLoading:</span> <span class="kc">false</span><span class="p">,</span> <span class="nl">errorMessage:</span> <span class="n">message</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>ErrorHandler</code> 集中管理所有錯誤碼到 i18n key 的映射，不讓這段邏輯散落在各個 ViewModel：</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="kd">class</span> <span class="nc">ErrorHandler</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">static</span> <span class="kt">String</span> <span class="n">getMessage</span><span class="p">(</span><span class="n">BookErrorCode</span> <span class="n">code</span><span class="p">,</span> <span class="p">{</span><span class="kd">required</span> <span class="n">AppLocalizations</span> <span class="n">l10n</span><span class="p">})</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">switch</span> <span class="p">(</span><span class="n">code</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">      <span class="k">case</span> <span class="n">BookErrorCode</span><span class="p">.</span><span class="nl">notFound:</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">return</span> <span class="n">l10n</span><span class="p">.</span><span class="n">bookNotFound</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="k">case</span> <span class="n">BookErrorCode</span><span class="p">.</span><span class="nl">invalidIsbn:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">return</span> <span class="n">l10n</span><span class="p">.</span><span class="n">invalidIsbn</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="k">case</span> <span class="n">BookErrorCode</span><span class="p">.</span><span class="nl">networkTimeout:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span> <span class="n">l10n</span><span class="p">.</span><span class="n">networkTimeout</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="k">case</span> <span class="n">BookErrorCode</span><span class="p">.</span><span class="nl">serverError:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span> <span class="n">l10n</span><span class="p">.</span><span class="n">serverError</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>動態參數的情況，ARB 檔案定義佔位符，ViewModel 負責組裝：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;bookNotFoundWithId&#34;</span><span class="p">:</span> <span class="s2">&#34;找不到書籍（ID: {bookId}）&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;@bookNotFoundWithId&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nt">&#34;placeholders&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">      <span class="nt">&#34;bookId&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;String&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<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="kd">final</span> <span class="n">message</span> <span class="o">=</span> <span class="n">l10n</span><span class="p">.</span><span class="n">bookNotFoundWithId</span><span class="p">(</span><span class="n">bookId</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">state</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">copyWith</span><span class="p">(</span><span class="nl">errorMessage:</span> <span class="n">message</span><span class="p">);</span></span></span></code></pre></div><h2 id="ui-層只負責顯示">UI 層：只負責顯示</h2>
<p>UI 層的工作極度單純：顯示 ViewModel 準備好的狀態，不判斷錯誤碼，不組裝字串：</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="kd">class</span> <span class="nc">BookDetailScreen</span> <span class="kd">extends</span> <span class="n">ConsumerWidget</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">,</span> <span class="n">WidgetRef</span> <span class="n">ref</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="kd">final</span> <span class="n">state</span> <span class="o">=</span> <span class="n">ref</span><span class="p">.</span><span class="n">watch</span><span class="p">(</span><span class="n">bookDetailViewModelProvider</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="n">isLoading</span><span class="p">)</span> <span class="k">return</span> <span class="kd">const</span> <span class="n">CircularProgressIndicator</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="n">errorMessage</span> <span class="o">!=</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="k">return</span> <span class="n">ErrorDisplay</span><span class="p">(</span><span class="nl">message:</span> <span class="n">state</span><span class="p">.</span><span class="n">errorMessage</span><span class="o">!</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="n">BookDetailContent</span><span class="p">(</span><span class="nl">book:</span> <span class="n">state</span><span class="p">.</span><span class="n">book</span><span class="o">!</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>build</code> 方法裡不應該出現任何 <code>switch (errorCode)</code> 或 <code>l10n.xxx</code> 來組裝錯誤訊息。</p>
<h2 id="常見反模式">常見反模式</h2>
<p><strong>Domain 層包含使用者訊息</strong>：repository 直接回傳 <code>'找不到這本書'</code>，要改成英文就得進業務邏輯層動手，Domain 層不應該對 UI 語言有任何感知。</p>
<p><strong>ViewModel 硬編碼訊息</strong>：<code>state.copyWith(errorMessage: '找不到書籍')</code> 讓字串散落各處，修改時不知道有幾個地方要同步改動。</p>
<p><strong>UI 層自行組裝訊息</strong>：Widget 的 <code>build</code> 方法裡出現 <code>switch (state.errorCode)</code>，同一段決策邏輯開始在不同 Widget 裡複製出現。</p>
<p><strong>跨層傳遞 BuildContext</strong>：把 <code>BuildContext</code> 傳入 Domain 層，Domain 層不應該依賴 Flutter 框架的任何概念，這樣的程式碼幾乎無法進行單元測試。</p>
<h2 id="實際效益">實際效益</h2>
<p>新增語言支援時，只需要在 ARB 檔案裡補翻譯，確認 <code>ErrorHandler</code> 映射完整，不需要搜索整個 codebase 找散落的字串。</p>
<p>Domain 層的測試變乾淨，只需驗證正確的 <code>ErrorCode</code> 被回傳，不用比對任何字串內容。每次在 Domain 層看到中文字串，或在 UI 層看到 i18n key 被呼叫，就是一個警示信號——訊息的生成和轉換，只屬於 ViewModel 層。</p>]]></content:encoded></item><item><title>MVVM ViewModel 開發方法論</title><link>https://tarrragon.github.io/blog/record/mvvm-viewmodel-%E9%96%8B%E7%99%BC%E6%96%B9%E6%B3%95%E8%AB%96/</link><pubDate>Mon, 13 Oct 2025 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/mvvm-viewmodel-%E9%96%8B%E7%99%BC%E6%96%B9%E6%B3%95%E8%AB%96/</guid><description>&lt;h2 id="前言">前言&lt;/h2>
&lt;p>為了提升AI開發前端的穩定性，我想依賴MVVM確實定義前端的狀態跟模型，以及責任分層，這樣可以降低除錯的複雜度&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="viewmodel-定位">ViewModel 定位&lt;/h3>
&lt;p>&lt;strong>ViewModel 是 MVVM 架構的核心層&lt;/strong>，負責：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Domain → UI 轉換&lt;/strong>：將 Domain 模型轉為 UI 需要的格式&lt;/li>
&lt;li>&lt;strong>UI 狀態管理&lt;/strong>：管理 Widget 狀態和互動邏輯&lt;/li>
&lt;li>&lt;strong>Provider 定義&lt;/strong>：定義 Riverpod Provider 供 Widget 使用&lt;/li>
&lt;li>&lt;strong>UI 專用計算邏輯&lt;/strong>：提供顏色、圖標、格式化文字等 UI 屬性&lt;/li>
&lt;/ol>
&lt;h3 id="mvvm-分層原則">MVVM 分層原則&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">┌─────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">│ Presentation Layer (UI 層) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">├─────────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">│ Widget (Page/Extensions) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">│ - 純 UI 組裝，無業務邏輯 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">│ - 使用 ViewModel Provider 取得資料 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">│ - 顯示 ViewModel 提供的 UI 屬性 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">├─────────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">│ ViewModel │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">│ - Domain → UI 轉換 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">│ - UI 狀態管理 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">│ - UI 專用計算邏輯 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">│ - Provider 定義 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">├─────────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">│ Mapper │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">│ - Domain 模型 → ViewModel 轉換邏輯 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">├─────────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">│ Domain Layer (領域層) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">│ - 業務邏輯 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">│ - Domain 模型 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">│ - Domain 服務 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">└─────────────────────────────────────────┘&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="viewmodel-命名規範">ViewModel 命名規範&lt;/h2>
&lt;h3 id="命名格式">命名格式&lt;/h3>
&lt;p>&lt;strong>格式&lt;/strong>：&lt;code>[Feature]ViewModel&lt;/code>&lt;/p>
&lt;p>&lt;strong>範例&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;code>EnrichmentProgressViewModel&lt;/code> - 補充進度顯示&lt;/li>
&lt;li>&lt;code>ChromeExtensionImportViewModel&lt;/code> - Chrome Extension 匯入&lt;/li>
&lt;li>&lt;code>LibraryDisplayViewModel&lt;/code> - 書庫展示&lt;/li>
&lt;li>&lt;code>AdvancedSearchViewModel&lt;/code> - 進階搜尋&lt;/li>
&lt;/ul>
&lt;h3 id="檔案位置">檔案位置&lt;/h3>
&lt;p>&lt;strong>標準路徑&lt;/strong>：&lt;code>lib/presentation/[feature]/[feature]_viewmodel.dart&lt;/code>&lt;/p>
&lt;p>&lt;strong>範例&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">lib/presentation/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">├── import/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">│ └── chrome_extension_import_viewmodel.dart
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">├── library/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">│ ├── library_viewmodel.dart
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">│ └── library_display_page.dart
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">└── search/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> └── advanced_search_viewmodel.dart&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="viewmodel-職責定義">ViewModel 職責定義&lt;/h2>
&lt;h3 id="包含的職責">包含的職責&lt;/h3>
&lt;h4 id="1-domain--ui-轉換">1. Domain → UI 轉換&lt;/h4>
&lt;p>將 Domain 模型轉換為 UI 需要的格式：&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">/// Domain 來源
&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="kd">final&lt;/span> &lt;span class="n">EnrichmentProgress&lt;/span> &lt;span class="n">domainProgress&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>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">/// UI 專用欄位（計算屬性）
&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="kt">String&lt;/span> &lt;span class="kd">get&lt;/span> &lt;span class="n">displayStatus&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">_mapStatus&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 class="n">IconData&lt;/span> &lt;span class="kd">get&lt;/span> &lt;span class="n">statusIcon&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">_mapIcon&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="n">Color&lt;/span> &lt;span class="kd">get&lt;/span> &lt;span class="n">progressColor&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">_mapColor&lt;/span>&lt;span class="p">();&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="2-ui-狀態管理">2. UI 狀態管理&lt;/h4>
&lt;p>管理 Widget 需要的狀態：&lt;/p></description><content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>為了提升AI開發前端的穩定性，我想依賴MVVM確實定義前端的狀態跟模型，以及責任分層，這樣可以降低除錯的複雜度</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="viewmodel-定位">ViewModel 定位</h3>
<p><strong>ViewModel 是 MVVM 架構的核心層</strong>，負責：</p>
<ol>
<li><strong>Domain → UI 轉換</strong>：將 Domain 模型轉為 UI 需要的格式</li>
<li><strong>UI 狀態管理</strong>：管理 Widget 狀態和互動邏輯</li>
<li><strong>Provider 定義</strong>：定義 Riverpod Provider 供 Widget 使用</li>
<li><strong>UI 專用計算邏輯</strong>：提供顏色、圖標、格式化文字等 UI 屬性</li>
</ol>
<h3 id="mvvm-分層原則">MVVM 分層原則</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">┌─────────────────────────────────────────┐
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">│ Presentation Layer (UI 層)              │
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">├─────────────────────────────────────────┤
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│ Widget (Page/Extensions)                │
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│ - 純 UI 組裝，無業務邏輯                 │
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│ - 使用 ViewModel Provider 取得資料       │
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│ - 顯示 ViewModel 提供的 UI 屬性          │
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">├─────────────────────────────────────────┤
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">│ ViewModel                               │
</span></span><span class="line"><span class="ln">10</span><span class="cl">│ - Domain → UI 轉換                       │
</span></span><span class="line"><span class="ln">11</span><span class="cl">│ - UI 狀態管理                            │
</span></span><span class="line"><span class="ln">12</span><span class="cl">│ - UI 專用計算邏輯                        │
</span></span><span class="line"><span class="ln">13</span><span class="cl">│ - Provider 定義                          │
</span></span><span class="line"><span class="ln">14</span><span class="cl">├─────────────────────────────────────────┤
</span></span><span class="line"><span class="ln">15</span><span class="cl">│ Mapper                                  │
</span></span><span class="line"><span class="ln">16</span><span class="cl">│ - Domain 模型 → ViewModel 轉換邏輯       │
</span></span><span class="line"><span class="ln">17</span><span class="cl">├─────────────────────────────────────────┤
</span></span><span class="line"><span class="ln">18</span><span class="cl">│ Domain Layer (領域層)                    │
</span></span><span class="line"><span class="ln">19</span><span class="cl">│ - 業務邏輯                               │
</span></span><span class="line"><span class="ln">20</span><span class="cl">│ - Domain 模型                            │
</span></span><span class="line"><span class="ln">21</span><span class="cl">│ - Domain 服務                            │
</span></span><span class="line"><span class="ln">22</span><span class="cl">└─────────────────────────────────────────┘</span></span></code></pre></div><hr>
<h2 id="viewmodel-命名規範">ViewModel 命名規範</h2>
<h3 id="命名格式">命名格式</h3>
<p><strong>格式</strong>：<code>[Feature]ViewModel</code></p>
<p><strong>範例</strong>：</p>
<ul>
<li><code>EnrichmentProgressViewModel</code> - 補充進度顯示</li>
<li><code>ChromeExtensionImportViewModel</code> - Chrome Extension 匯入</li>
<li><code>LibraryDisplayViewModel</code> - 書庫展示</li>
<li><code>AdvancedSearchViewModel</code> - 進階搜尋</li>
</ul>
<h3 id="檔案位置">檔案位置</h3>
<p><strong>標準路徑</strong>：<code>lib/presentation/[feature]/[feature]_viewmodel.dart</code></p>
<p><strong>範例</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">lib/presentation/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── import/
</span></span><span class="line"><span class="ln">3</span><span class="cl">│   └── chrome_extension_import_viewmodel.dart
</span></span><span class="line"><span class="ln">4</span><span class="cl">├── library/
</span></span><span class="line"><span class="ln">5</span><span class="cl">│   ├── library_viewmodel.dart
</span></span><span class="line"><span class="ln">6</span><span class="cl">│   └── library_display_page.dart
</span></span><span class="line"><span class="ln">7</span><span class="cl">└── search/
</span></span><span class="line"><span class="ln">8</span><span class="cl">    └── advanced_search_viewmodel.dart</span></span></code></pre></div><hr>
<h2 id="viewmodel-職責定義">ViewModel 職責定義</h2>
<h3 id="包含的職責">包含的職責</h3>
<h4 id="1-domain--ui-轉換">1. Domain → UI 轉換</h4>
<p>將 Domain 模型轉換為 UI 需要的格式：</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">/// Domain 來源
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">EnrichmentProgress</span> <span class="n">domainProgress</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">/// UI 專用欄位（計算屬性）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kt">String</span> <span class="kd">get</span> <span class="n">displayStatus</span> <span class="o">=&gt;</span> <span class="n">_mapStatus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">IconData</span> <span class="kd">get</span> <span class="n">statusIcon</span> <span class="o">=&gt;</span> <span class="n">_mapIcon</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">Color</span> <span class="kd">get</span> <span class="n">progressColor</span> <span class="o">=&gt;</span> <span class="n">_mapColor</span><span class="p">();</span></span></span></code></pre></div><h4 id="2-ui-狀態管理">2. UI 狀態管理</h4>
<p>管理 Widget 需要的狀態：</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="kd">class</span> <span class="nc">EnrichmentProgressViewModel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// 狀態欄位
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">EnrichmentProgress</span> <span class="n">domainProgress</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kd">final</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Book</span><span class="o">&gt;</span> <span class="n">failedBooks</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="c1">// UI 控制狀態
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="kt">bool</span> <span class="kd">get</span> <span class="n">showFailedBooks</span> <span class="o">=&gt;</span> <span class="n">failedBooks</span><span class="p">.</span><span class="n">isNotEmpty</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="kt">bool</span> <span class="kd">get</span> <span class="n">canRetry</span> <span class="o">=&gt;</span> <span class="n">domainProgress</span><span class="p">.</span><span class="n">isComplete</span> <span class="o">&amp;&amp;</span> <span class="n">failedBooks</span><span class="p">.</span><span class="n">isNotEmpty</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h4 id="3-provider-定義">3. Provider 定義</h4>
<p>定義 Riverpod Provider 供 Widget 使用：</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="kd">final</span> <span class="n">enrichmentProgressViewModelProvider</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">StreamProvider</span><span class="p">.</span><span class="n">family</span><span class="o">&lt;</span><span class="n">EnrichmentProgressViewModel</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="p">(</span><span class="n">ref</span><span class="p">,</span> <span class="n">operationId</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">      <span class="c1">// Provider 邏輯
</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 class="p">);</span></span></span></code></pre></div><h4 id="4-ui-專用計算邏輯">4. UI 專用計算邏輯</h4>
<p>提供 UI 需要的格式化資料：</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="kt">String</span> <span class="kd">get</span> <span class="n">summaryText</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="s1">&#39;已處理 </span><span class="si">${</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">processedBooks</span><span class="si">}</span><span class="s1">/</span><span class="si">${</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">totalBooks</span><span class="si">}</span><span class="s1"> 本&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">/// 進度顏色（根據狀態決定）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="n">Color</span> <span class="kd">get</span> <span class="n">progressColor</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">failedEnrichments</span> <span class="o">&gt;</span> <span class="m">0</span><span class="p">)</span> <span class="k">return</span> <span class="n">Colors</span><span class="p">.</span><span class="n">orange</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">isComplete</span><span class="p">)</span> <span class="k">return</span> <span class="n">Colors</span><span class="p">.</span><span class="n">green</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">return</span> <span class="n">Colors</span><span class="p">.</span><span class="n">blue</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="不包含的內容">不包含的內容</h3>
<h4 id="1-widget-程式碼">1. Widget 程式碼</h4>





<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">// 反例：ViewModel 中包含 Widget
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">EnrichmentProgressViewModel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">Widget</span> <span class="n">buildProgressBar</span><span class="p">()</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="k">return</span> <span class="n">LinearProgressIndicator</span><span class="p">(...);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 正例：Widget 在 Extension 中
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="n">extension</span> <span class="n">EnrichmentProgressWidgets</span> <span class="n">on</span> <span class="n">Widget</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="n">Widget</span> <span class="n">enrichmentProgressBar</span><span class="p">(</span><span class="n">EnrichmentProgressViewModel</span> <span class="n">vm</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">return</span> <span class="n">LinearProgressIndicator</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">      <span class="nl">value:</span> <span class="n">vm</span><span class="p">.</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">percentageComplete</span> <span class="o">/</span> <span class="m">100</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">      <span class="nl">color:</span> <span class="n">vm</span><span class="p">.</span><span class="n">progressColor</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h4 id="2-直接依賴-flutter-框架除-changenotifier">2. 直接依賴 Flutter 框架（除 ChangeNotifier）</h4>





<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">// 反例：依賴 Flutter Material
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">import</span> <span class="s1">&#39;package:flutter/material.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kd">class</span> <span class="nc">MyViewModel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="n">BuildContext</span><span class="o">?</span> <span class="n">context</span><span class="p">;</span>  <span class="c1">// 違規
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 正例：使用 Dart 原生類型
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">MyViewModel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="n">Color</span> <span class="n">progressColor</span><span class="p">;</span>  <span class="c1">// 可以使用 Color（來自 dart:ui）
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span>  <span class="n">IconData</span> <span class="n">statusIcon</span><span class="p">;</span>  <span class="c1">// 可以使用 IconData
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><h4 id="3-業務邏輯">3. 業務邏輯</h4>





<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">// 反例：在 ViewModel 中執行業務邏輯
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">EnrichmentProgressViewModel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span> <span class="n">enrichBook</span><span class="p">(</span><span class="n">Book</span> <span class="n">book</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="c1">// 呼叫 API、驗證資料、儲存到資料庫
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>    <span class="c1">// 這些是 Domain 層的職責
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">// 正例：業務邏輯在 Domain Service
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">IBookInfoEnrichmentService</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="n">Future</span><span class="o">&lt;</span><span class="n">EnrichedBookInfo</span><span class="o">&gt;</span> <span class="n">enrichBookInfo</span><span class="p">(</span><span class="n">Book</span> <span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">// ViewModel 只負責狀態管理
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">EnrichmentProgressViewModel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="kd">final</span> <span class="n">EnrichmentProgress</span> <span class="n">domainProgress</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="c1">// 不包含業務邏輯
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="viewmodel-結構範本">ViewModel 結構範本</h2>
<h3 id="基本結構">基本結構</h3>





<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">/// UI 層專用的 [Feature] 顯示模型
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">///
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 職責：
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">/// - 將 Domain 模型轉換為 UI 需要的格式
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">/// - 提供 UI 專用的計算屬性
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">/// - 管理 UI 狀態
</span></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></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="err">[</span><span class="nc">Feature</span><span class="p">]</span><span class="n">ViewModel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span>  <span class="c1">// Domain 來源（不可變）
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span>  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="c1">/// Domain 模型來源
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="p">[</span><span class="n">DomainModel</span><span class="p">]</span> <span class="n">domainModel</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="c1">/// 額外的 Domain 資料（如失敗清單）
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">List</span><span class="o">&lt;</span><span class="p">[</span><span class="n">Entity</span><span class="p">]</span><span class="o">&gt;</span> <span class="n">additionalData</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"></span>  <span class="c1">// UI 專用欄位（計算屬性）
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"></span>  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="c1">/// 狀態顯示文字
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="kd">get</span> <span class="n">displayStatus</span> <span class="o">=&gt;</span> <span class="n">_mapStatus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="c1">/// 狀態圖標
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"></span>  <span class="n">IconData</span> <span class="kd">get</span> <span class="n">statusIcon</span> <span class="o">=&gt;</span> <span class="n">_mapIcon</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">  <span class="c1">/// 進度顏色
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="c1"></span>  <span class="n">Color</span> <span class="kd">get</span> <span class="n">progressColor</span> <span class="o">=&gt;</span> <span class="n">_mapColor</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl">  <span class="c1">/// 摘要文字
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="kd">get</span> <span class="n">summaryText</span> <span class="o">=&gt;</span> <span class="n">_formatSummary</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">
</span></span><span class="line"><span class="ln">36</span><span class="cl">  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="c1"></span>  <span class="c1">// 建構子
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="c1"></span>  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">  <span class="kd">const</span> <span class="p">[</span><span class="n">Feature</span><span class="p">]</span><span class="n">ViewModel</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">domainModel</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="n">additionalData</span> <span class="o">=</span> <span class="kd">const</span> <span class="p">[],</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">
</span></span><span class="line"><span class="ln">45</span><span class="cl">  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln">46</span><span class="cl"><span class="c1"></span>  <span class="c1">// Domain → UI 轉換方法（私有）
</span></span></span><span class="line"><span class="ln">47</span><span class="cl"><span class="c1"></span>  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">  <span class="c1">/// 對應狀態到顯示文字
</span></span></span><span class="line"><span class="ln">50</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="n">_mapStatus</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">    <span class="c1">// 轉換邏輯
</span></span></span><span class="line"><span class="ln">52</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">
</span></span><span class="line"><span class="ln">54</span><span class="cl">  <span class="c1">/// 對應狀態到圖標
</span></span></span><span class="line"><span class="ln">55</span><span class="cl"><span class="c1"></span>  <span class="n">IconData</span> <span class="n">_mapIcon</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">    <span class="c1">// 轉換邏輯
</span></span></span><span class="line"><span class="ln">57</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">
</span></span><span class="line"><span class="ln">59</span><span class="cl">  <span class="c1">/// 對應狀態到顏色
</span></span></span><span class="line"><span class="ln">60</span><span class="cl"><span class="c1"></span>  <span class="n">Color</span> <span class="n">_mapColor</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl">    <span class="c1">// 轉換邏輯
</span></span></span><span class="line"><span class="ln">62</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">
</span></span><span class="line"><span class="ln">64</span><span class="cl">  <span class="c1">/// 格式化摘要文字
</span></span></span><span class="line"><span class="ln">65</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="n">_formatSummary</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl">    <span class="c1">// 格式化邏輯
</span></span></span><span class="line"><span class="ln">67</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln">68</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="完整範例enrichmentprogressviewmodel">完整範例：EnrichmentProgressViewModel</h3>





<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="k">import</span> <span class="s1">&#39;package:flutter/material.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  2</span><span class="cl"><span class="k">import</span> <span class="s1">&#39;package:book_overview_app/domains/import/value_objects/enrichment_progress.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  3</span><span class="cl"><span class="k">import</span> <span class="s1">&#39;package:book_overview_app/domains/library/entities/book.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  4</span><span class="cl">
</span></span><span class="line"><span class="ln">  5</span><span class="cl"><span class="c1">/// UI 層專用的補充進度顯示模型
</span></span></span><span class="line"><span class="ln">  6</span><span class="cl"><span class="c1">///
</span></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">/// - 將 EnrichmentProgress Domain 模型轉為 UI 格式
</span></span></span><span class="line"><span class="ln">  9</span><span class="cl"><span class="c1">/// - 提供進度顏色、圖標、文字等 UI 屬性
</span></span></span><span class="line"><span class="ln"> 10</span><span class="cl"><span class="c1">/// - 管理失敗書籍清單的顯示
</span></span></span><span class="line"><span class="ln"> 11</span><span class="cl"><span class="c1">///
</span></span></span><span class="line"><span class="ln"> 12</span><span class="cl"><span class="c1">/// 需求：UC-01.Enrichment.Progress
</span></span></span><span class="line"><span class="ln"> 13</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">EnrichmentProgressViewModel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 14</span><span class="cl">  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln"> 15</span><span class="cl"><span class="c1"></span>  <span class="c1">// Domain 來源
</span></span></span><span class="line"><span class="ln"> 16</span><span class="cl"><span class="c1"></span>  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln"> 17</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 18</span><span class="cl">  <span class="c1">/// Domain 進度模型
</span></span></span><span class="line"><span class="ln"> 19</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">EnrichmentProgress</span> <span class="n">domainProgress</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 20</span><span class="cl">
</span></span><span class="line"><span class="ln"> 21</span><span class="cl">  <span class="c1">/// 失敗補充的書籍清單
</span></span></span><span class="line"><span class="ln"> 22</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Book</span><span class="o">&gt;</span> <span class="n">failedBooks</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 23</span><span class="cl">
</span></span><span class="line"><span class="ln"> 24</span><span class="cl">  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln"> 25</span><span class="cl"><span class="c1"></span>  <span class="c1">// UI 專用欄位（計算屬性）
</span></span></span><span class="line"><span class="ln"> 26</span><span class="cl"><span class="c1"></span>  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln"> 27</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 28</span><span class="cl">  <span class="c1">/// 狀態顯示文字
</span></span></span><span class="line"><span class="ln"> 29</span><span class="cl"><span class="c1"></span>  <span class="c1">///
</span></span></span><span class="line"><span class="ln"> 30</span><span class="cl"><span class="c1"></span>  <span class="c1">/// 對應規則：
</span></span></span><span class="line"><span class="ln"> 31</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - processedBooks == 0 → &#34;準備中&#34;
</span></span></span><span class="line"><span class="ln"> 32</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - processedBooks &gt; 0 &amp;&amp; !isComplete → &#34;補充中&#34;
</span></span></span><span class="line"><span class="ln"> 33</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - isComplete → &#34;已完成&#34;
</span></span></span><span class="line"><span class="ln"> 34</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="kd">get</span> <span class="n">displayStatus</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 35</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">isComplete</span><span class="p">)</span> <span class="k">return</span> <span class="s1">&#39;已完成&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 36</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">processedBooks</span> <span class="o">==</span> <span class="m">0</span><span class="p">)</span> <span class="k">return</span> <span class="s1">&#39;準備中&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 37</span><span class="cl">    <span class="k">return</span> <span class="s1">&#39;補充中&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 38</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 39</span><span class="cl">
</span></span><span class="line"><span class="ln"> 40</span><span class="cl">  <span class="c1">/// 狀態圖標
</span></span></span><span class="line"><span class="ln"> 41</span><span class="cl"><span class="c1"></span>  <span class="c1">///
</span></span></span><span class="line"><span class="ln"> 42</span><span class="cl"><span class="c1"></span>  <span class="c1">/// 對應規則：
</span></span></span><span class="line"><span class="ln"> 43</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - 準備中 → Icons.pending
</span></span></span><span class="line"><span class="ln"> 44</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - 補充中 → Icons.sync
</span></span></span><span class="line"><span class="ln"> 45</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - 已完成 → Icons.check_circle
</span></span></span><span class="line"><span class="ln"> 46</span><span class="cl"><span class="c1"></span>  <span class="n">IconData</span> <span class="kd">get</span> <span class="n">statusIcon</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 47</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">isComplete</span><span class="p">)</span> <span class="k">return</span> <span class="n">Icons</span><span class="p">.</span><span class="n">check_circle</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 48</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">processedBooks</span> <span class="o">==</span> <span class="m">0</span><span class="p">)</span> <span class="k">return</span> <span class="n">Icons</span><span class="p">.</span><span class="n">pending</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 49</span><span class="cl">    <span class="k">return</span> <span class="n">Icons</span><span class="p">.</span><span class="kd">sync</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 50</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 51</span><span class="cl">
</span></span><span class="line"><span class="ln"> 52</span><span class="cl">  <span class="c1">/// 進度顏色
</span></span></span><span class="line"><span class="ln"> 53</span><span class="cl"><span class="c1"></span>  <span class="c1">///
</span></span></span><span class="line"><span class="ln"> 54</span><span class="cl"><span class="c1"></span>  <span class="c1">/// 對應規則：
</span></span></span><span class="line"><span class="ln"> 55</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - 有失敗 → 橘色警告
</span></span></span><span class="line"><span class="ln"> 56</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - 已完成 → 綠色成功
</span></span></span><span class="line"><span class="ln"> 57</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - 進行中 → 藍色
</span></span></span><span class="line"><span class="ln"> 58</span><span class="cl"><span class="c1"></span>  <span class="n">Color</span> <span class="kd">get</span> <span class="n">progressColor</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 59</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">failedEnrichments</span> <span class="o">&gt;</span> <span class="m">0</span><span class="p">)</span> <span class="k">return</span> <span class="n">Colors</span><span class="p">.</span><span class="n">orange</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 60</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">domainProgress</span><span class="p">.</span><span class="n">isComplete</span><span class="p">)</span> <span class="k">return</span> <span class="n">Colors</span><span class="p">.</span><span class="n">green</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 61</span><span class="cl">    <span class="k">return</span> <span class="n">Colors</span><span class="p">.</span><span class="n">blue</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 62</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 63</span><span class="cl">
</span></span><span class="line"><span class="ln"> 64</span><span class="cl">  <span class="c1">/// 摘要文字
</span></span></span><span class="line"><span class="ln"> 65</span><span class="cl"><span class="c1"></span>  <span class="c1">///
</span></span></span><span class="line"><span class="ln"> 66</span><span class="cl"><span class="c1"></span>  <span class="c1">/// 格式：「已處理 X/Y 本（成功 A，失敗 B）」
</span></span></span><span class="line"><span class="ln"> 67</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="kd">get</span> <span class="n">summaryText</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 68</span><span class="cl">    <span class="kd">final</span> <span class="n">processed</span> <span class="o">=</span> <span class="n">domainProgress</span><span class="p">.</span><span class="n">processedBooks</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 69</span><span class="cl">    <span class="kd">final</span> <span class="n">total</span> <span class="o">=</span> <span class="n">domainProgress</span><span class="p">.</span><span class="n">totalBooks</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 70</span><span class="cl">    <span class="kd">final</span> <span class="n">success</span> <span class="o">=</span> <span class="n">domainProgress</span><span class="p">.</span><span class="n">successfulEnrichments</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 71</span><span class="cl">    <span class="kd">final</span> <span class="n">failed</span> <span class="o">=</span> <span class="n">domainProgress</span><span class="p">.</span><span class="n">failedEnrichments</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 72</span><span class="cl">
</span></span><span class="line"><span class="ln"> 73</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">failed</span> <span class="o">&gt;</span> <span class="m">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 74</span><span class="cl">      <span class="k">return</span> <span class="s1">&#39;已處理 </span><span class="si">$</span><span class="n">processed</span><span class="s1">/</span><span class="si">$</span><span class="n">total</span><span class="s1"> 本（成功 </span><span class="si">$</span><span class="n">success</span><span class="s1">，失敗 </span><span class="si">$</span><span class="n">failed</span><span class="s1">）&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 75</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 76</span><span class="cl">    <span class="k">return</span> <span class="s1">&#39;已處理 </span><span class="si">$</span><span class="n">processed</span><span class="s1">/</span><span class="si">$</span><span class="n">total</span><span class="s1"> 本&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 77</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 78</span><span class="cl">
</span></span><span class="line"><span class="ln"> 79</span><span class="cl">  <span class="c1">/// 失敗書籍摘要清單
</span></span></span><span class="line"><span class="ln"> 80</span><span class="cl"><span class="c1"></span>  <span class="c1">///
</span></span></span><span class="line"><span class="ln"> 81</span><span class="cl"><span class="c1"></span>  <span class="c1">/// 提供簡化的書籍資訊供 UI 顯示
</span></span></span><span class="line"><span class="ln"> 82</span><span class="cl"><span class="c1"></span>  <span class="n">List</span><span class="o">&lt;</span><span class="n">BookSummary</span><span class="o">&gt;</span> <span class="kd">get</span> <span class="n">failedBooksSummary</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 83</span><span class="cl">    <span class="k">return</span> <span class="n">failedBooks</span><span class="p">.</span><span class="n">map</span><span class="p">((</span><span class="n">book</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">BookSummary</span><span class="p">.</span><span class="n">fromBook</span><span class="p">(</span><span class="n">book</span><span class="p">)).</span><span class="n">toList</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 84</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 85</span><span class="cl">
</span></span><span class="line"><span class="ln"> 86</span><span class="cl">  <span class="c1">/// 進度百分比（直接使用 Domain 計算）
</span></span></span><span class="line"><span class="ln"> 87</span><span class="cl"><span class="c1"></span>  <span class="kt">double</span> <span class="kd">get</span> <span class="n">progressPercentage</span> <span class="o">=&gt;</span> <span class="n">domainProgress</span><span class="p">.</span><span class="n">percentageComplete</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 88</span><span class="cl">
</span></span><span class="line"><span class="ln"> 89</span><span class="cl">  <span class="c1">/// 當前處理書名（如果有）
</span></span></span><span class="line"><span class="ln"> 90</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span><span class="o">?</span> <span class="kd">get</span> <span class="n">currentBookTitle</span> <span class="o">=&gt;</span> <span class="n">domainProgress</span><span class="p">.</span><span class="n">currentBook</span><span class="o">?</span><span class="p">.</span><span class="n">title</span><span class="p">.</span><span class="n">value</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 91</span><span class="cl">
</span></span><span class="line"><span class="ln"> 92</span><span class="cl">  <span class="c1">/// 是否顯示失敗清單
</span></span></span><span class="line"><span class="ln"> 93</span><span class="cl"><span class="c1"></span>  <span class="kt">bool</span> <span class="kd">get</span> <span class="n">showFailedBooks</span> <span class="o">=&gt;</span> <span class="n">failedBooks</span><span class="p">.</span><span class="n">isNotEmpty</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 94</span><span class="cl">
</span></span><span class="line"><span class="ln"> 95</span><span class="cl">  <span class="c1">/// 是否可以重試
</span></span></span><span class="line"><span class="ln"> 96</span><span class="cl"><span class="c1"></span>  <span class="kt">bool</span> <span class="kd">get</span> <span class="n">canRetry</span> <span class="o">=&gt;</span> <span class="n">domainProgress</span><span class="p">.</span><span class="n">isComplete</span> <span class="o">&amp;&amp;</span> <span class="n">failedBooks</span><span class="p">.</span><span class="n">isNotEmpty</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 97</span><span class="cl">
</span></span><span class="line"><span class="ln"> 98</span><span class="cl">  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln"> 99</span><span class="cl"><span class="c1"></span>  <span class="c1">// 建構子
</span></span></span><span class="line"><span class="ln">100</span><span class="cl"><span class="c1"></span>  <span class="c1">// =============================================================================
</span></span></span><span class="line"><span class="ln">101</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">102</span><span class="cl">  <span class="kd">const</span> <span class="n">EnrichmentProgressViewModel</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">103</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">domainProgress</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">104</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="n">failedBooks</span> <span class="o">=</span> <span class="kd">const</span> <span class="p">[],</span>
</span></span><span class="line"><span class="ln">105</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">106</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">107</span><span class="cl">
</span></span><span class="line"><span class="ln">108</span><span class="cl"><span class="c1">/// 書籍摘要（UI 專用簡化資料）
</span></span></span><span class="line"><span class="ln">109</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">BookSummary</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">110</span><span class="cl">  <span class="kd">final</span> <span class="kt">String</span> <span class="n">id</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">111</span><span class="cl">  <span class="kd">final</span> <span class="kt">String</span> <span class="n">title</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">112</span><span class="cl">  <span class="kd">final</span> <span class="kt">String</span> <span class="n">author</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">113</span><span class="cl">
</span></span><span class="line"><span class="ln">114</span><span class="cl">  <span class="kd">const</span> <span class="n">BookSummary</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">115</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">id</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">116</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">title</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">117</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">author</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">118</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">119</span><span class="cl">
</span></span><span class="line"><span class="ln">120</span><span class="cl">  <span class="kd">factory</span> <span class="n">BookSummary</span><span class="p">.</span><span class="n">fromBook</span><span class="p">(</span><span class="n">Book</span> <span class="n">book</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">121</span><span class="cl">    <span class="k">return</span> <span class="n">BookSummary</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">122</span><span class="cl">      <span class="nl">id:</span> <span class="n">book</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">value</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">123</span><span class="cl">      <span class="nl">title:</span> <span class="n">book</span><span class="p">.</span><span class="n">title</span><span class="p">.</span><span class="n">value</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">124</span><span class="cl">      <span class="nl">author:</span> <span class="n">book</span><span class="p">.</span><span class="n">author</span><span class="p">.</span><span class="n">value</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">125</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">126</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">127</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="mapper-模式">Mapper 模式</h2>
<h3 id="mapper-職責">Mapper 職責</h3>
<p><strong>Mapper 負責 Domain 模型 → ViewModel 的轉換邏輯</strong>。</p>
<h3 id="mapper-結構">Mapper 結構</h3>





<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">/// Domain [DomainModel] → UI ViewModel 轉換器
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">///
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 職責：
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">/// - 將 Domain 模型轉換為 ViewModel
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">/// - 整合多個 Domain 資料來源
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">/// - 處理轉換過程中的資料格式化
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="err">[</span><span class="nc">Feature</span><span class="p">]</span><span class="n">Mapper</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="c1">/// 轉換 Domain 模型為 ViewModel
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="kd">static</span> <span class="p">[</span><span class="n">Feature</span><span class="p">]</span><span class="n">ViewModel</span> <span class="n">toViewModel</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">[</span><span class="n">DomainModel</span><span class="p">]</span> <span class="n">domainModel</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1">// 額外的 Domain 資料來源
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span>  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="n">Feature</span><span class="p">]</span><span class="n">ViewModel</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">      <span class="nl">domainModel:</span> <span class="n">domainModel</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">      <span class="c1">// 額外欄位轉換
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span>    <span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="c1">/// 批量轉換
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"></span>  <span class="kd">static</span> <span class="n">List</span><span class="o">&lt;</span><span class="p">[</span><span class="n">Feature</span><span class="p">]</span><span class="n">ViewModel</span><span class="o">&gt;</span> <span class="n">toViewModelList</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="n">List</span><span class="o">&lt;</span><span class="p">[</span><span class="n">DomainModel</span><span class="p">]</span><span class="o">&gt;</span> <span class="n">domainModels</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="k">return</span> <span class="n">domainModels</span><span class="p">.</span><span class="n">map</span><span class="p">((</span><span class="n">model</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">toViewModel</span><span class="p">(</span><span class="n">model</span><span class="p">)).</span><span class="n">toList</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="完整範例enrichmentprogressmapper">完整範例：EnrichmentProgressMapper</h3>





<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="k">import</span> <span class="s1">&#39;package:book_overview_app/domains/import/value_objects/enrichment_progress.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">import</span> <span class="s1">&#39;package:book_overview_app/domains/library/entities/book.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">import</span> <span class="s1">&#39;package:book_overview_app/presentation/import/enrichment_progress_viewmodel.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">/// Domain EnrichmentProgress → UI ViewModel 轉換器
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">///
</span></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">/// - 整合 EnrichmentProgress 和失敗書籍清單
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">/// - 轉換為 UI 層需要的 ViewModel 格式
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">EnrichmentProgressMapper</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="c1">/// 轉換 Domain 進度模型為 ViewModel
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span>  <span class="c1">///
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="c1">/// 參數：
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - [progress]: Domain 進度模型
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span>  <span class="c1">/// - [failedBooks]: 失敗補充的書籍清單（從 Service 取得）
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span>  <span class="c1">///
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"></span>  <span class="c1">/// 回傳：UI 層專用的 ViewModel
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span>  <span class="kd">static</span> <span class="n">EnrichmentProgressViewModel</span> <span class="n">toViewModel</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">EnrichmentProgress</span> <span class="n">progress</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="n">List</span><span class="o">&lt;</span><span class="n">Book</span><span class="o">&gt;</span> <span class="n">failedBooks</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">return</span> <span class="n">EnrichmentProgressViewModel</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">      <span class="nl">domainProgress:</span> <span class="n">progress</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">      <span class="nl">failedBooks:</span> <span class="n">failedBooks</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="c1">/// 批量轉換（如果需要顯示多個進度）
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="c1"></span>  <span class="kd">static</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">EnrichmentProgressViewModel</span><span class="o">&gt;</span> <span class="n">toViewModelList</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="n">List</span><span class="o">&lt;</span><span class="n">EnrichmentProgress</span><span class="o">&gt;</span> <span class="n">progressList</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Book</span><span class="o">&gt;&gt;</span> <span class="n">failedBooksMap</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="k">return</span> <span class="n">progressList</span><span class="p">.</span><span class="n">map</span><span class="p">((</span><span class="n">progress</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">      <span class="c1">// 假設每個 progress 有唯一 ID
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="c1"></span>      <span class="kd">final</span> <span class="n">failedBooks</span> <span class="o">=</span> <span class="n">failedBooksMap</span><span class="p">[</span><span class="n">progress</span><span class="p">.</span><span class="n">hashCode</span><span class="p">.</span><span class="n">toString</span><span class="p">()]</span> <span class="o">??</span> <span class="p">[];</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">      <span class="k">return</span> <span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="n">failedBooks</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="p">}).</span><span class="n">toList</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="provider-整合模式">Provider 整合模式</h2>
<h3 id="streamprovider-整合">StreamProvider 整合</h3>
<p><strong>當 Domain 資料是 Stream 時使用 StreamProvider</strong>。</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">/// ViewModel StreamProvider 定義
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">///
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 職責：
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">/// - 整合多個 Domain Provider
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">/// - 使用 Mapper 轉換為 ViewModel
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">/// - 提供給 Widget 使用
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">enrichmentProgressViewModelProvider</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="n">StreamProvider</span><span class="p">.</span><span class="n">family</span><span class="o">&lt;</span><span class="n">EnrichmentProgressViewModel</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">(</span><span class="n">ref</span><span class="p">,</span> <span class="n">operationId</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="c1">// 1. 監聽 Domain Progress Stream
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span>      <span class="kd">final</span> <span class="n">domainProgressStream</span> <span class="o">=</span> <span class="n">ref</span><span class="p">.</span><span class="n">watch</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">enrichmentProgressProvider</span><span class="p">(</span><span class="n">operationId</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">      <span class="c1">// 2. 監聽失敗書籍 Stream
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span>      <span class="kd">final</span> <span class="n">failedBooksStream</span> <span class="o">=</span> <span class="n">ref</span><span class="p">.</span><span class="n">watch</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">failedBooksProvider</span><span class="p">(</span><span class="n">operationId</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">      <span class="c1">// 3. 合併 Stream 並轉換為 ViewModel
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"></span>      <span class="k">return</span> <span class="n">Rx</span><span class="p">.</span><span class="n">combineLatest2</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="n">domainProgressStream</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="n">failedBooksStream</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="p">(</span><span class="n">EnrichmentProgress</span> <span class="n">progress</span><span class="p">,</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Book</span><span class="o">&gt;</span> <span class="n">failedBooks</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">          <span class="k">return</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">            <span class="n">progress</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="n">failedBooks</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">          <span class="p">);</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">  <span class="p">);</span></span></span></code></pre></div><h3 id="stateprovider-整合">StateProvider 整合</h3>
<p><strong>當 ViewModel 需要狀態管理時使用 Notifier</strong>。</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">/// ViewModel State 定義
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">LibraryDisplayState</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">final</span> <span class="n">DisplayMode</span> <span class="n">displayMode</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">final</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">LibraryBookModel</span><span class="o">&gt;</span> <span class="n">books</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">final</span> <span class="n">Set</span><span class="o">&lt;</span><span class="kt">String</span><span class="o">&gt;</span> <span class="n">selectedBookIds</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="kd">const</span> <span class="n">LibraryDisplayState</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="n">displayMode</span> <span class="o">=</span> <span class="n">DisplayMode</span><span class="p">.</span><span class="n">simple</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="n">books</span> <span class="o">=</span> <span class="kd">const</span> <span class="p">[],</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">this</span><span class="p">.</span><span class="n">selectedBookIds</span> <span class="o">=</span> <span class="kd">const</span> <span class="p">{},</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="n">LibraryDisplayState</span> <span class="n">copyWith</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">DisplayMode</span><span class="o">?</span> <span class="n">displayMode</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">List</span><span class="o">&lt;</span><span class="n">LibraryBookModel</span><span class="o">&gt;?</span> <span class="n">books</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">Set</span><span class="o">&lt;</span><span class="kt">String</span><span class="o">&gt;?</span> <span class="n">selectedBookIds</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="p">})</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">return</span> <span class="n">LibraryDisplayState</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">      <span class="nl">displayMode:</span> <span class="n">displayMode</span> <span class="o">??</span> <span class="k">this</span><span class="p">.</span><span class="n">displayMode</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">      <span class="nl">books:</span> <span class="n">books</span> <span class="o">??</span> <span class="k">this</span><span class="p">.</span><span class="n">books</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">      <span class="nl">selectedBookIds:</span> <span class="n">selectedBookIds</span> <span class="o">??</span> <span class="k">this</span><span class="p">.</span><span class="n">selectedBookIds</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1">/// ViewModel Notifier
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">LibraryDisplayViewModel</span> <span class="kd">extends</span> <span class="n">Notifier</span><span class="o">&lt;</span><span class="n">LibraryDisplayState</span><span class="o">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">  <span class="n">LibraryDisplayState</span> <span class="n">build</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="k">return</span> <span class="kd">const</span> <span class="n">LibraryDisplayState</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl">  <span class="c1">/// 切換顯示模式
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="c1"></span>  <span class="kt">void</span> <span class="n">toggleDisplayMode</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="kd">final</span> <span class="n">newMode</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">displayMode</span> <span class="o">==</span> <span class="n">DisplayMode</span><span class="p">.</span><span class="n">simple</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="o">?</span> <span class="n">DisplayMode</span><span class="p">.</span><span class="n">management</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="o">:</span> <span class="n">DisplayMode</span><span class="p">.</span><span class="n">simple</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">    <span class="n">state</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">copyWith</span><span class="p">(</span><span class="nl">displayMode:</span> <span class="n">newMode</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">
</span></span><span class="line"><span class="ln">41</span><span class="cl">  <span class="c1">/// 選擇書籍
</span></span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="c1"></span>  <span class="kt">void</span> <span class="n">toggleBookSelection</span><span class="p">(</span><span class="kt">String</span> <span class="n">bookId</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">    <span class="kd">final</span> <span class="n">selectedIds</span> <span class="o">=</span> <span class="n">Set</span><span class="o">&lt;</span><span class="kt">String</span><span class="o">&gt;</span><span class="p">.</span><span class="n">from</span><span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="n">selectedBookIds</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">selectedIds</span><span class="p">.</span><span class="n">contains</span><span class="p">(</span><span class="n">bookId</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">      <span class="n">selectedIds</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="n">bookId</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">      <span class="n">selectedIds</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">bookId</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">    <span class="n">state</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">copyWith</span><span class="p">(</span><span class="nl">selectedBookIds:</span> <span class="n">selectedIds</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">
</span></span><span class="line"><span class="ln">53</span><span class="cl"><span class="c1">/// Provider 定義
</span></span></span><span class="line"><span class="ln">54</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">libraryDisplayViewModelProvider</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">  <span class="n">NotifierProvider</span><span class="o">&lt;</span><span class="n">LibraryDisplayViewModel</span><span class="p">,</span> <span class="n">LibraryDisplayState</span><span class="o">&gt;</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">    <span class="n">LibraryDisplayViewModel</span><span class="p">.</span><span class="k">new</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">  <span class="p">);</span></span></span></code></pre></div><hr>
<h2 id="widget-使用方式">Widget 使用方式</h2>
<h3 id="streamprovider-使用">StreamProvider 使用</h3>





<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="kd">class</span> <span class="nc">EnrichmentProgressWidget</span> <span class="kd">extends</span> <span class="n">ConsumerWidget</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">final</span> <span class="kt">String</span> <span class="n">operationId</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">const</span> <span class="n">EnrichmentProgressWidget</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">operationId</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">super</span><span class="p">.</span><span class="n">key</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">,</span> <span class="n">WidgetRef</span> <span class="n">ref</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="kd">final</span> <span class="n">viewModelAsync</span> <span class="o">=</span> <span class="n">ref</span><span class="p">.</span><span class="n">watch</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">      <span class="n">enrichmentProgressViewModelProvider</span><span class="p">(</span><span class="n">operationId</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="n">viewModelAsync</span><span class="p">.</span><span class="n">when</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="nl">data:</span> <span class="p">(</span><span class="n">viewModel</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">_buildProgressContent</span><span class="p">(</span><span class="n">viewModel</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">      <span class="nl">loading:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kd">const</span> <span class="n">CircularProgressIndicator</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="nl">error:</span> <span class="p">(</span><span class="n">error</span><span class="p">,</span> <span class="n">stack</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">ErrorWidget</span><span class="p">(</span><span class="n">error</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="n">Widget</span> <span class="n">_buildProgressContent</span><span class="p">(</span><span class="n">EnrichmentProgressViewModel</span> <span class="n">vm</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="k">return</span> <span class="n">Column</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">      <span class="nl">children:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="c1">// 使用 ViewModel 的 UI 屬性
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"></span>        <span class="n">Icon</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">statusIcon</span><span class="p">,</span> <span class="nl">color:</span> <span class="n">vm</span><span class="p">.</span><span class="n">progressColor</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="n">Text</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">displayStatus</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="n">LinearProgressIndicator</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">          <span class="nl">value:</span> <span class="n">vm</span><span class="p">.</span><span class="n">progressPercentage</span> <span class="o">/</span> <span class="m">100</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">          <span class="nl">color:</span> <span class="n">vm</span><span class="p">.</span><span class="n">progressColor</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="n">Text</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">summaryText</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="c1">// 失敗清單
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="c1"></span>        <span class="k">if</span> <span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">showFailedBooks</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">          <span class="n">_buildFailedBooksList</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">failedBooksSummary</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">      <span class="p">],</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="stateprovider-使用">StateProvider 使用</h3>





<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="kd">class</span> <span class="nc">LibraryDisplayPage</span> <span class="kd">extends</span> <span class="n">ConsumerWidget</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">Widget</span> <span class="n">build</span><span class="p">(</span><span class="n">BuildContext</span> <span class="n">context</span><span class="p">,</span> <span class="n">WidgetRef</span> <span class="n">ref</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="kd">final</span> <span class="n">state</span> <span class="o">=</span> <span class="n">ref</span><span class="p">.</span><span class="n">watch</span><span class="p">(</span><span class="n">libraryDisplayViewModelProvider</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">final</span> <span class="n">viewModel</span> <span class="o">=</span> <span class="n">ref</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">libraryDisplayViewModelProvider</span><span class="p">.</span><span class="n">notifier</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="k">return</span> <span class="n">Scaffold</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="nl">appBar:</span> <span class="n">AppBar</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nl">title:</span> <span class="n">Text</span><span class="p">(</span><span class="s1">&#39;書庫&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nl">actions:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">          <span class="n">IconButton</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="nl">icon:</span> <span class="n">Icon</span><span class="p">(</span><span class="n">Icons</span><span class="p">.</span><span class="n">view_list</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="nl">onPressed:</span> <span class="n">viewModel</span><span class="p">.</span><span class="n">toggleDisplayMode</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">          <span class="p">),</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="p">],</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">      <span class="nl">body:</span> <span class="n">ListView</span><span class="p">.</span><span class="n">builder</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nl">itemCount:</span> <span class="n">state</span><span class="p">.</span><span class="n">books</span><span class="p">.</span><span class="n">length</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="nl">itemBuilder:</span> <span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">index</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">          <span class="kd">final</span> <span class="n">book</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">books</span><span class="p">[</span><span class="n">index</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">          <span class="kd">final</span> <span class="n">isSelected</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">selectedBookIds</span><span class="p">.</span><span class="n">contains</span><span class="p">(</span><span class="n">book</span><span class="p">.</span><span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">          <span class="k">return</span> <span class="n">ListTile</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">            <span class="nl">title:</span> <span class="n">Text</span><span class="p">(</span><span class="n">book</span><span class="p">.</span><span class="n">title</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">            <span class="nl">selected:</span> <span class="n">isSelected</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">            <span class="nl">onTap:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="n">viewModel</span><span class="p">.</span><span class="n">toggleBookSelection</span><span class="p">(</span><span class="n">book</span><span class="p">.</span><span class="n">id</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">          <span class="p">);</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="測試要求">測試要求</h2>
<h3 id="單元測試覆蓋率">單元測試覆蓋率</h3>
<p><strong>每個 ViewModel 必須有單元測試，覆蓋率 ≥ 90%</strong>。</p>
<h3 id="測試項目">測試項目</h3>
<ol>
<li><strong>Domain → UI 轉換邏輯</strong></li>
<li><strong>UI 專用計算邏輯</strong></li>
<li><strong>狀態管理邏輯</strong>（如果是 Notifier）</li>
<li><strong>邊界條件和錯誤處理</strong></li>
</ol>
<h3 id="測試範例">測試範例</h3>





<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="k">import</span> <span class="s1">&#39;package:flutter_test/flutter_test.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  2</span><span class="cl"><span class="k">import</span> <span class="s1">&#39;package:book_overview_app/domains/import/value_objects/enrichment_progress.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  3</span><span class="cl"><span class="k">import</span> <span class="s1">&#39;package:book_overview_app/presentation/import/enrichment_progress_viewmodel.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  4</span><span class="cl"><span class="k">import</span> <span class="s1">&#39;package:book_overview_app/presentation/import/enrichment_progress_mapper.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  5</span><span class="cl">
</span></span><span class="line"><span class="ln">  6</span><span class="cl"><span class="kt">void</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">  7</span><span class="cl">  <span class="n">group</span><span class="p">(</span><span class="s1">&#39;EnrichmentProgressViewModel&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">  8</span><span class="cl">    <span class="n">group</span><span class="p">(</span><span class="s1">&#39;displayStatus&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">  9</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;準備中 - processedBooks == 0&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 10</span><span class="cl">        <span class="c1">// Arrange
</span></span></span><span class="line"><span class="ln"> 11</span><span class="cl"><span class="c1"></span>        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">.</span><span class="n">initial</span><span class="p">(</span><span class="m">10</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 12</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln"> 13</span><span class="cl">
</span></span><span class="line"><span class="ln"> 14</span><span class="cl">        <span class="c1">// Act &amp; Assert
</span></span></span><span class="line"><span class="ln"> 15</span><span class="cl"><span class="c1"></span>        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">displayStatus</span><span class="p">,</span> <span class="s1">&#39;準備中&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 16</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 17</span><span class="cl">
</span></span><span class="line"><span class="ln"> 18</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;補充中 - processedBooks &gt; 0 且未完成&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 19</span><span class="cl">        <span class="c1">// Arrange
</span></span></span><span class="line"><span class="ln"> 20</span><span class="cl"><span class="c1"></span>        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 21</span><span class="cl">          <span class="nl">totalBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 22</span><span class="cl">          <span class="nl">processedBooks:</span> <span class="m">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 23</span><span class="cl">          <span class="nl">successfulEnrichments:</span> <span class="m">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 24</span><span class="cl">          <span class="nl">failedEnrichments:</span> <span class="m">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 25</span><span class="cl">        <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 26</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln"> 27</span><span class="cl">
</span></span><span class="line"><span class="ln"> 28</span><span class="cl">        <span class="c1">// Act &amp; Assert
</span></span></span><span class="line"><span class="ln"> 29</span><span class="cl"><span class="c1"></span>        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">displayStatus</span><span class="p">,</span> <span class="s1">&#39;補充中&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 30</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 31</span><span class="cl">
</span></span><span class="line"><span class="ln"> 32</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;已完成 - processedBooks == totalBooks&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 33</span><span class="cl">        <span class="c1">// Arrange
</span></span></span><span class="line"><span class="ln"> 34</span><span class="cl"><span class="c1"></span>        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 35</span><span class="cl">          <span class="nl">totalBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 36</span><span class="cl">          <span class="nl">processedBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 37</span><span class="cl">          <span class="nl">successfulEnrichments:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 38</span><span class="cl">          <span class="nl">failedEnrichments:</span> <span class="m">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 39</span><span class="cl">        <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 40</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln"> 41</span><span class="cl">
</span></span><span class="line"><span class="ln"> 42</span><span class="cl">        <span class="c1">// Act &amp; Assert
</span></span></span><span class="line"><span class="ln"> 43</span><span class="cl"><span class="c1"></span>        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">displayStatus</span><span class="p">,</span> <span class="s1">&#39;已完成&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 44</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 45</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 46</span><span class="cl">
</span></span><span class="line"><span class="ln"> 47</span><span class="cl">    <span class="n">group</span><span class="p">(</span><span class="s1">&#39;statusIcon&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 48</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;準備中 - Icons.pending&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 49</span><span class="cl">        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">.</span><span class="n">initial</span><span class="p">(</span><span class="m">10</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 50</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln"> 51</span><span class="cl">
</span></span><span class="line"><span class="ln"> 52</span><span class="cl">        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">statusIcon</span><span class="p">,</span> <span class="n">Icons</span><span class="p">.</span><span class="n">pending</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 53</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 54</span><span class="cl">
</span></span><span class="line"><span class="ln"> 55</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;補充中 - Icons.sync&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 56</span><span class="cl">        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 57</span><span class="cl">          <span class="nl">totalBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 58</span><span class="cl">          <span class="nl">processedBooks:</span> <span class="m">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 59</span><span class="cl">          <span class="nl">successfulEnrichments:</span> <span class="m">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 60</span><span class="cl">          <span class="nl">failedEnrichments:</span> <span class="m">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 61</span><span class="cl">        <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 62</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln"> 63</span><span class="cl">
</span></span><span class="line"><span class="ln"> 64</span><span class="cl">        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">statusIcon</span><span class="p">,</span> <span class="n">Icons</span><span class="p">.</span><span class="kd">sync</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 65</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 66</span><span class="cl">
</span></span><span class="line"><span class="ln"> 67</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;已完成 - Icons.check_circle&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 68</span><span class="cl">        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 69</span><span class="cl">          <span class="nl">totalBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 70</span><span class="cl">          <span class="nl">processedBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 71</span><span class="cl">          <span class="nl">successfulEnrichments:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 72</span><span class="cl">          <span class="nl">failedEnrichments:</span> <span class="m">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 73</span><span class="cl">        <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 74</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln"> 75</span><span class="cl">
</span></span><span class="line"><span class="ln"> 76</span><span class="cl">        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">statusIcon</span><span class="p">,</span> <span class="n">Icons</span><span class="p">.</span><span class="n">check_circle</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 77</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 78</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 79</span><span class="cl">
</span></span><span class="line"><span class="ln"> 80</span><span class="cl">    <span class="n">group</span><span class="p">(</span><span class="s1">&#39;progressColor&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 81</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;有失敗 - Colors.orange&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 82</span><span class="cl">        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 83</span><span class="cl">          <span class="nl">totalBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 84</span><span class="cl">          <span class="nl">processedBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 85</span><span class="cl">          <span class="nl">successfulEnrichments:</span> <span class="m">8</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 86</span><span class="cl">          <span class="nl">failedEnrichments:</span> <span class="m">2</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 87</span><span class="cl">        <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 88</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln"> 89</span><span class="cl">
</span></span><span class="line"><span class="ln"> 90</span><span class="cl">        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">progressColor</span><span class="p">,</span> <span class="n">Colors</span><span class="p">.</span><span class="n">orange</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 91</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 92</span><span class="cl">
</span></span><span class="line"><span class="ln"> 93</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;已完成無失敗 - Colors.green&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 94</span><span class="cl">        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 95</span><span class="cl">          <span class="nl">totalBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 96</span><span class="cl">          <span class="nl">processedBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 97</span><span class="cl">          <span class="nl">successfulEnrichments:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 98</span><span class="cl">          <span class="nl">failedEnrichments:</span> <span class="m">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 99</span><span class="cl">        <span class="p">);</span>
</span></span><span class="line"><span class="ln">100</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln">101</span><span class="cl">
</span></span><span class="line"><span class="ln">102</span><span class="cl">        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">progressColor</span><span class="p">,</span> <span class="n">Colors</span><span class="p">.</span><span class="n">green</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">103</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln">104</span><span class="cl">
</span></span><span class="line"><span class="ln">105</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;進行中 - Colors.blue&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">106</span><span class="cl">        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">107</span><span class="cl">          <span class="nl">totalBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">108</span><span class="cl">          <span class="nl">processedBooks:</span> <span class="m">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">109</span><span class="cl">          <span class="nl">successfulEnrichments:</span> <span class="m">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">110</span><span class="cl">          <span class="nl">failedEnrichments:</span> <span class="m">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">111</span><span class="cl">        <span class="p">);</span>
</span></span><span class="line"><span class="ln">112</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln">113</span><span class="cl">
</span></span><span class="line"><span class="ln">114</span><span class="cl">        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">progressColor</span><span class="p">,</span> <span class="n">Colors</span><span class="p">.</span><span class="n">blue</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">115</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln">116</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">117</span><span class="cl">
</span></span><span class="line"><span class="ln">118</span><span class="cl">    <span class="n">group</span><span class="p">(</span><span class="s1">&#39;summaryText&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">119</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;無失敗 - 顯示處理進度&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">120</span><span class="cl">        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">121</span><span class="cl">          <span class="nl">totalBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">122</span><span class="cl">          <span class="nl">processedBooks:</span> <span class="m">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">123</span><span class="cl">          <span class="nl">successfulEnrichments:</span> <span class="m">5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">124</span><span class="cl">          <span class="nl">failedEnrichments:</span> <span class="m">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">125</span><span class="cl">        <span class="p">);</span>
</span></span><span class="line"><span class="ln">126</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln">127</span><span class="cl">
</span></span><span class="line"><span class="ln">128</span><span class="cl">        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">summaryText</span><span class="p">,</span> <span class="s1">&#39;已處理 5/10 本&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">129</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln">130</span><span class="cl">
</span></span><span class="line"><span class="ln">131</span><span class="cl">      <span class="n">test</span><span class="p">(</span><span class="s1">&#39;有失敗 - 顯示成功和失敗數&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">132</span><span class="cl">        <span class="kd">final</span> <span class="n">progress</span> <span class="o">=</span> <span class="n">EnrichmentProgress</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">133</span><span class="cl">          <span class="nl">totalBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">134</span><span class="cl">          <span class="nl">processedBooks:</span> <span class="m">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">135</span><span class="cl">          <span class="nl">successfulEnrichments:</span> <span class="m">8</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">136</span><span class="cl">          <span class="nl">failedEnrichments:</span> <span class="m">2</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">137</span><span class="cl">        <span class="p">);</span>
</span></span><span class="line"><span class="ln">138</span><span class="cl">        <span class="kd">final</span> <span class="n">vm</span> <span class="o">=</span> <span class="n">EnrichmentProgressMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln">139</span><span class="cl">
</span></span><span class="line"><span class="ln">140</span><span class="cl">        <span class="n">expect</span><span class="p">(</span><span class="n">vm</span><span class="p">.</span><span class="n">summaryText</span><span class="p">,</span> <span class="s1">&#39;已處理 10/10 本（成功 8，失敗 2）&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">141</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln">142</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">143</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">144</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="viewmodel-開發檢查清單">ViewModel 開發檢查清單</h2>
<h3 id="phase-1-設計階段">Phase 1: 設計階段</h3>
<ul>
<li><input disabled="" type="checkbox"> 確認 Domain 模型已完成</li>
<li><input disabled="" type="checkbox"> 定義 ViewModel 需要的 UI 屬性</li>
<li><input disabled="" type="checkbox"> 設計 Domain → UI 轉換邏輯</li>
<li><input disabled="" type="checkbox"> 規劃 Mapper 結構</li>
<li><input disabled="" type="checkbox"> 定義 Provider 整合方式</li>
</ul>
<h3 id="phase-2-實作階段">Phase 2: 實作階段</h3>
<ul>
<li><input disabled="" type="checkbox"> 建立 ViewModel 類別和欄位</li>
<li><input disabled="" type="checkbox"> 實作 UI 專用計算屬性</li>
<li><input disabled="" type="checkbox"> 實作 Mapper 轉換方法</li>
<li><input disabled="" type="checkbox"> 定義 Provider</li>
<li><input disabled="" type="checkbox"> 撰寫完整註解（包含需求編號）</li>
</ul>
<h3 id="phase-3-測試階段">Phase 3: 測試階段</h3>
<ul>
<li><input disabled="" type="checkbox"> 撰寫 ViewModel 單元測試</li>
<li><input disabled="" type="checkbox"> 測試所有計算屬性</li>
<li><input disabled="" type="checkbox"> 測試 Mapper 轉換邏輯</li>
<li><input disabled="" type="checkbox"> 測試邊界條件</li>
<li><input disabled="" type="checkbox"> 達成 90% 以上覆蓋率</li>
</ul>
<h3 id="phase-4-整合階段">Phase 4: 整合階段</h3>
<ul>
<li><input disabled="" type="checkbox"> Widget 整合 ViewModel Provider</li>
<li><input disabled="" type="checkbox"> 驗證 UI 正確顯示</li>
<li><input disabled="" type="checkbox"> 執行 Widget 測試</li>
<li><input disabled="" type="checkbox"> Code Review 確認符合規範</li>
</ul>
<hr>
<h2 id="常見問題和最佳實踐">常見問題和最佳實踐</h2>
<h3 id="q1-viewmodel-可以包含-statefulwidget-的狀態嗎">Q1: ViewModel 可以包含 StatefulWidget 的狀態嗎？</h3>
<p><strong>A</strong>: 不可以。ViewModel 應該是純資料模型，不包含 Widget 生命週期邏輯。</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="kd">class</span> <span class="nc">MyViewModel</span> <span class="kd">extends</span> <span class="n">StatefulWidget</span> <span class="p">{</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 正例
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">MyViewModel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="kd">final</span> <span class="n">MyDomainModel</span> <span class="n">domainModel</span><span class="p">;</span>
</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="p">}</span></span></span></code></pre></div><h3 id="q2-如何處理複雜的-ui-狀態">Q2: 如何處理複雜的 UI 狀態？</h3>
<p><strong>A</strong>: 使用 Notifier 管理狀態，定義 State 類別。</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="kd">class</span> <span class="nc">MyState</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">final</span> <span class="n">DisplayMode</span> <span class="n">mode</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">final</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">items</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">final</span> <span class="n">Set</span><span class="o">&lt;</span><span class="kt">String</span><span class="o">&gt;</span> <span class="n">selectedIds</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="n">MyState</span> <span class="n">copyWith</span><span class="p">({...})</span> <span class="p">{</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">class</span> <span class="nc">MyViewModel</span> <span class="kd">extends</span> <span class="n">Notifier</span><span class="o">&lt;</span><span class="n">MyState</span><span class="o">&gt;</span> <span class="p">{</span> <span class="p">}</span></span></span></code></pre></div><h3 id="q3-viewmodel-可以呼叫-domain-service-嗎">Q3: ViewModel 可以呼叫 Domain Service 嗎？</h3>
<p><strong>A</strong>: 可以，但建議透過 Provider 整合而非直接呼叫。</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">// 推薦：透過 Provider 整合
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">myViewModelProvider</span> <span class="o">=</span> <span class="n">Provider</span><span class="p">((</span><span class="n">ref</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">final</span> <span class="n">domainData</span> <span class="o">=</span> <span class="n">ref</span><span class="p">.</span><span class="n">watch</span><span class="p">(</span><span class="n">domainServiceProvider</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">return</span> <span class="n">MyMapper</span><span class="p">.</span><span class="n">toViewModel</span><span class="p">(</span><span class="n">domainData</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><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="kd">class</span> <span class="nc">MyViewModel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="kd">final</span> <span class="n">MyDomainService</span> <span class="n">service</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span> <span class="n">fetchData</span><span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="kd">final</span> <span class="n">data</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">service</span><span class="p">.</span><span class="n">getData</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="q4-多個-domain-模型如何整合到一個-viewmodel">Q4: 多個 Domain 模型如何整合到一個 ViewModel？</h3>
<p><strong>A</strong>: 在 Mapper 中整合多個來源。</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="kd">class</span> <span class="nc">MyMapper</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">static</span> <span class="n">MyViewModel</span> <span class="n">toViewModel</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">DomainModel1</span> <span class="n">model1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">DomainModel2</span> <span class="n">model2</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">List</span><span class="o">&lt;</span><span class="n">Entity</span><span class="o">&gt;</span> <span class="n">entities</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">return</span> <span class="n">MyViewModel</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="nl">field1:</span> <span class="n">model1</span><span class="p">.</span><span class="n">value</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="nl">field2:</span> <span class="n">model2</span><span class="p">.</span><span class="n">value</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="nl">items:</span> <span class="n">entities</span><span class="p">.</span><span class="n">map</span><span class="p">(</span><span class="n">_mapEntity</span><span class="p">).</span><span class="n">toList</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>]]></content:encoded></item></channel></rss>