架構設計模式
如果你已經閱讀過 架構指南 頁面,或是你對 Flutter 以及 MVVM 模式已經相當熟悉, 那麼以下這些文章將非常適合你。
這些文章並非著重於高層次的應用程式架構,而是針對特定設計問題提供解決方案, 無論你如何設計你的應用程式架構,都能幫助你優化應用程式的程式碼基礎。 不過,這些文章在程式碼範例中,仍假設你已採用前面頁面所介紹的 MVVM 模式。
樂觀狀態(Optimistic state)
透過實作樂觀狀態,提升應用程式的響應式體驗。
在建立使用者體驗時, 效能的「感知」有時和實際程式碼效能一樣重要。 一般來說,使用者不喜歡等待動作完成才看到結果, 而任何超過幾毫秒的延遲,從使用者角度來看,都可能被認為是「慢」或「沒有回應」。
開發者可以透過在背景任務尚未完全完成前, 先呈現成功的 UI 狀態,來減輕這種負面感受。 例如,當點擊「訂閱」按鈕時, 即使背景的訂閱 API 呼叫尚在執行, 也能立即將按鈕狀態變為「已訂閱」。
這種技巧稱為樂觀狀態(Optimistic State)、樂觀 UI(Optimistic UI)或 樂觀使用者體驗(Optimistic User Experience)。 在本教學中, 你將會使用樂觀狀態來實作一個應用程式功能, 並遵循Flutter 架構指引。
範例功能:訂閱按鈕
#這個範例實作了一個訂閱按鈕, 類似於你在影音串流應用程式或電子報中會看到的設計。

當按鈕被點擊時,應用程式會呼叫外部 API, 執行訂閱動作, 例如在資料庫中記錄該使用者已加入訂閱名單。 為了示範,本範例不會實作實際的後端程式碼, 而是用一個假的動作來模擬網路請求。
如果呼叫成功, 按鈕文字會從「訂閱」變成「已訂閱」, 按鈕背景顏色也會改變。
相反地,如果呼叫失敗, 按鈕文字應該恢復為「訂閱」, 並且 UI 會顯示錯誤訊息給使用者, 例如使用 Snackbar。
根據樂觀狀態的概念, 按鈕在被點擊時應立即變為「已訂閱」, 只有在請求失敗時才恢復為「訂閱」。

功能架構
#首先,請定義此功能的架構。 依照架構指引, 在 Flutter 專案中建立以下 Dart 類別:
- 一個名為
SubscribeButton的StatefulWidget... 閱讀完整文章
持久化儲存架構:鍵值資料
將應用程式資料儲存到使用者裝置上的鍵值儲存區。
大多數 Flutter 應用程式,無論規模大小,都會在某個時刻需要將資料儲存到使用者的裝置上,例如 API 金鑰、使用者偏好設定,或是需要離線可用的資料。
在本教學中,你將學習如何在採用推薦 Flutter 架構設計 的 Flutter 應用程式中,整合鍵值資料的持久化儲存。如果你對於如何將資料儲存到磁碟還不熟悉,可以先閱讀 將鍵值資料儲存到磁碟 這篇教學。
鍵值儲存區常用於儲存簡單資料,例如應用程式設定。在本教學中,你將用它來儲存深色模式(Dark Mode)偏好設定。如果你想學習如何在裝置上儲存複雜資料,建議使用 SQL。此時可參考本教學後續的 持久化儲存架構:SQL。
範例應用程式:可選主題的應用
#這個範例應用程式包含一個單一螢幕,上方有應用程式列(app bar)、中間是項目清單,底部有一個文字欄位(text field)輸入區。

在 AppBar 中,Switch 讓使用者可以在深色與淺色主題模式間切換。這個設定會立即套用,並透過鍵值資料儲存服務儲存在裝置上。當使用者再次啟動應用程式時,設定會自動還原。

儲存主題選擇的鍵值資料
#此功能遵循推薦的 Flutter 架構設計模式,分為展示層(presentation layer)與資料層(data layer)。
- 展示層包含
ThemeSwitch元件(Widget)與ThemeSwitchViewModel。 - 資料層包含
ThemeRepository與SharedPreferencesService。 ... 閱讀完整文章
持久化儲存架構:SQL
使用 SQL 將複雜的應用程式資料儲存到使用者裝置。
大多數 Flutter 應用程式,無論規模大小,最終都可能需要將資料儲存在使用者的裝置上。例如,API 金鑰、使用者偏好設定,或是應該可離線存取的資料。
在本教學中,你將學習如何在遵循 Flutter 架構設計模式的 Flutter 應用程式中,使用 SQL 整合複雜資料的持久化儲存。
若你想了解如何儲存較簡單的鍵值資料,請參考 Cookbook 教學:持久化儲存架構:鍵值資料。
閱讀本教學前,你應該已熟悉 SQL 與 SQLite。如果需要協助,建議先閱讀 使用 SQLite 持久化資料 教學。
本範例使用 sqflite 搭配 sqflite_common_ffi 套件,兩者結合可同時支援行動裝置與桌面端。Web 支援則由實驗性套件 sqflite_common_ffi_web 提供,但本範例未涵蓋。
範例應用程式:待辦清單應用程式
#本範例應用程式包含單一螢幕,頂部有 app bar,中間為項目清單,底部則有文字欄位 (text field) 輸入區。

應用程式主體包含 TodoListScreen。此螢幕包含一個 ListView,其內為 ListTile 項目,每個項目代表一個待辦事項 (ToDo item)。在底部,TextField 讓使用者可以輸入任務描述,然後點擊 “Add” FilledButton 來建立新的待辦事項。
使用者可以點擊刪除 IconButton 來刪除待辦事項。
待辦事項清單會透過資料庫服務儲存在本地端,並在使用者啟動應用程式時還原。
使用 SQL... 閱讀完整文章
離線優先支援
為應用程式中的一個功能實作離線優先支援。
離線優先(offline-first)應用程式是一種即使在斷線狀態下,仍能提供大部分或全部功能的應用程式。離線優先應用程式通常依賴已儲存的資料,讓使用者能暫時存取原本僅能線上取得的資料。
有些離線優先應用程式能無縫結合本地與遠端資料,而有些應用程式則會在使用快取資料時通知使用者。同樣地,有些應用程式會在背景自動同步資料,而有些則需要使用者明確執行同步。這一切取決於應用程式的需求與所提供的功能,由開發者決定哪種實作方式最符合需求。
在本指南中,你將學習如何在 Flutter 中,依循Flutter 架構指引,實作不同的離線優先應用程式方案。
離線優先架構
#如常見架構概念指南所述,repository(儲存庫)扮演單一真實資料來源(single source of truth)的角色。它們負責呈現本地或遠端資料,並且應該是唯一能修改資料的地方。在離線優先應用程式中,repository 會結合不同的本地與遠端資料來源,無論裝置的連線狀態如何,都能在單一存取點提供資料。
本範例使用 UserProfileRepository,這是一個支援離線優先的 repository,可用來取得與儲存 UserProfile 物件。
UserProfileRepository 使用了兩種不同的資料服務:一種處理遠端資料,另一種則操作本地資料庫。
API client ApiClientService 會透過 HTTP REST 呼叫連接至遠端服務。
class ApiClientService { /// performs GET network request to obtain a UserProfile Future<UserProfile> getUserProfile() async { // ··· } ... 閱讀完整文章
指令模式
透過實作 Command 類別,簡化 view model 邏輯。
Model-View-ViewModel (MVVM) 是一種設計模式, 將應用程式的一個功能分為三個部分: model(模型)、view model(檢視模型)以及 view(檢視)。 View 和 view model 組成應用程式的 UI 層。 Repository(儲存庫)和 service(服務)則代表應用程式的資料層, 也就是 MVVM 的 model 層。
Command(指令)是一個包裝方法的類別, 用來協助處理該方法的不同狀態, 例如執行中、完成與錯誤。
View model 可以使用 command 來處理互動與執行動作。 你也可以利用它們來顯示不同的 UI 狀態, 像是在動作執行時顯示載入指示器, 或是在動作失敗時顯示錯誤對話框。
隨著應用程式規模成長、功能變得更複雜, view model 也可能變得非常複雜。 Command 可以協助簡化 view model, 並重複利用程式碼。
在本指南中,你將學習 如何運用指令模式(command pattern) 來優化你的 view model。
實作 view model 時的挑戰
# 在 Flutter 中,view model 類別通常是 透過繼承 ChangeNotifier 類別來實作。 這讓 view model 可以呼叫 notifyListeners() 來刷新 view, 當資料被更新時。
dart<pre class="shiki shiki-themes... 閱讀完整文章
使用 Result 物件進行錯誤處理
透過 Result 物件提升跨類別的錯誤處理能力。
Dart 提供了內建的錯誤處理機制, 能夠拋出與捕捉例外(exceptions)。
如同在 錯誤處理文件 中所提到, Dart 的例外屬於未處理例外(unhandled exceptions)。 這表示會拋出例外的方法不需要宣告這些例外, 而呼叫這些方法的程式碼也不強制要求一定要捕捉它們。
這可能導致例外沒有被妥善處理的情況。 在大型專案中, 開發者可能會忘記捕捉例外, 而應用程式的不同層或元件 也可能會拋出未被記錄的例外。 這會導致錯誤發生甚至應用程式崩潰。
在本指南中, 你將會了解這項限制, 以及如何透過 result(結果)模式來改善。
Flutter 應用程式中的錯誤流程
# 遵循 Flutter 架構指引 的應用程式 通常由 view model、repository、service 等組件構成。 當這些元件中的某個函式發生失敗時, 應該將錯誤資訊傳遞給呼叫端元件。
通常這會透過例外來完成。 舉例來說, 當 API client service 無法與遠端伺服器溝通時, 可能會拋出 HTTP Error Exception。 呼叫端元件(例如 Repository), 必須選擇要捕捉這個例外, 或者忽略它並讓呼叫的 view model 處理。
這可以從以下範例觀察到。請參考這些類別:
- 一個 service,
ApiClientService,負責對遠端服務進行 API 呼叫。 - 一個 repository,
UserProfileRepository, 提供由 ApiClientService 所提供的 UserProfile。 - 一個 view model,
UserProfileViewModel,使用 UserProfileRepository。
ApiClientService 包含一個方法 getUserProfile, 在特定情境下會拋出例外:
- 當回應碼不是 200 時,該方法會拋出
HttpException。 - 若回應的 JSON 格式不正確,解析方法會拋出例外。
- HTTP client 也可能因網路問題而拋出例外。
以下程式碼測試了多種可能發生的例外情境:
... 閱讀完整文章