iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Mobile Development

《30 天 Flutter:跨平台 AI 行程規劃 App》系列 第 20

Day 20 - Drift 實戰:告別手動更新,讓 UI 畫面自動同步

  • 分享至 

  • xImage
  •  

前幾天在處理資料庫時,每當新增、更新或刪除資料後,我都得手動呼叫 ref.invalidateSelf(),讓 Riverpod 重新計算 provider,進而刷新 UI。這種命令式(imperative)的寫法雖然有效,但不僅寫起來繁瑣,也容易因為忘記呼叫而出錯。

我開始思考,有沒有更「聰明」的方式?直到深入研究 Drift,才發現它提供了串流查詢(Stream Queries) 功能。我可以把查詢結果包裝成一個 Stream,再透過 Riverpod 的 StreamProvider 綁定到 UI。這樣一來,只要資料庫有任何變動,串流就會自動發出最新結果,UI 也能立即收到更新,而不需要我再手動觸發。

今天,我就來分享這段從命令式到響應式的轉變歷程~


串流查詢:優缺點分析

優點

  • 即時自動更新:這是串流查詢最核心的優勢。當你將查詢結果轉為 Stream 後,Drift 會自動監聽相關的資料表。只要有新增、更新或刪除發生,串流就會發出最新的查詢結果。若再搭配 Riverpod 的 StreamProvider,UI 會自動收到狀態更新,開發者就不需要手動呼叫 ref.invalidateSelf()
  • 避免不必要的查詢:相較於在每次畫面重建或事件發生時都主動查詢一次資料庫,串流查詢只會在資料實際變動時才重新執行 SQL。這樣能避免過多的冗餘查詢,讓應用程式更有效率。
  • 程式碼更簡潔:由於資料變動與 UI 更新透過串流自動串接起來,程式碼不需要額外的手動刷新邏輯,更符合單一職責原則,也讓維護更容易。

缺點

  • 潛在的效能開銷:每當資料表內容變動時,相關串流都會重新執行查詢並推送結果。如果資料庫更新頻率極高,或查詢本身非常複雜,可能導致效能下降,並造成 UI 頻繁重繪。不過在行程規劃這類應用中,資料變動通常不算頻繁,因此影響並不明顯。

適用情境與注意事項:

  • 適合需要即時資料同步的應用,例如行程規劃、聊天訊息、股票報價等場景。
  • 如果資料變動頻率非常高或查詢非常複雜,串流頻繁推送更新可能會造成效能瓶頸,需要搭配 debouncethrottle 或分頁等策略。
  • 對於單次查詢或資料更新極少的場景,使用串流可能過度設計,反而增加複雜度。

從命令式到響應式:將 Drift 查詢轉為串流

為了將應用程式從手動更新的命令式寫法,徹底轉變為自動響應的串流架構,需要在三個主要環節進行調整:Drift DAORiverpod NotifierProvider 宣告

調整 Drift DAO 檔案

這是所有變動的起點。雖然 Drift 仍然可以使用 get() 進行一次性查詢,但在這裡的重點是改用監聽型 API,讓資料庫一有變動就能即時推送更新。

// trips_dao.dart

Stream<List<Trip>> watchTripsWithActivitiesAndChildren() {
  final query = select(tripsTable).join([
    // ... join 邏輯 ...
  ]);

  return query.watch().map((rows) {
    // 這裡的 tripList 是由 rows 轉換而來,範例中省略轉換邏輯以保持簡潔
    return tripList;
  });
}
  • 將用於獲取行程資料的 get...() 方法,改名為 watch...()(這是常見的命名慣例)。
  • 將回傳類型從 Future<List<T>> 改為 Stream<List<T>>
  • 在查詢中,用 query.watch() 替換原有的 await query.get()

轉換 Riverpod Notifier

這是管理應用程式狀態的核心,需要將其從 AsyncNotifier 轉為專門支援串流的 StreamNotifier

// trip_provider.dart

class TripListNotifier extends StreamNotifier<List<Trip>> {
  @override
  Stream<List<Trip>> build() {
     return db.tripsDao.watchTripsWithActivitiesAndChildren();
  }

  Future<void> addTrip(String title) async {
    await ref.read(appDatabaseProvider).tripsDao.addTrip(title);
    // ref.invalidateSelf(); // 不再需要
  }
}
  • 將類別從 AsyncNotifier<List<Trip>> 改為 StreamNotifier<List<Trip>>
  • build 函式直接回傳 DAO 的串流,不需要 async/await
  • 為了確保資料庫實例來源一致,應透過 ref.read(appDatabaseProvider) 取得,而不是直接 AppDatabase()
  • 在這裡要特別注意:UI 更新的原因不是「Drift 自動刷新 UI」,而是 DAO 的串流會持續推送新資料,Riverpod 再把這些更新同步到 UI。

更新 Provider 的宣告

最後一步,必須讓 Riverpod 知道現在管理的是一個「持續輸出的狀態」,而非一次性計算完成的狀態。

// trip_provider.dart

final tripListProvider = StreamNotifierProvider<TripListNotifier, List<Trip>>(
  () => TripListNotifier(),
);
  • 將 Provider 類型從 AsyncNotifierProvider 改為 StreamNotifierProviderStreamNotifierProvider 不會只計算一次結果,而是會持續監聽並輸出新的狀態。

要不要我幫你再補一個「修改前 vs 修改後」的對照表?這樣讀者會一眼看懂從 FutureStream,以及 AsyncNotifierStreamNotifier 的差別。

心得成果

在這次轉換之前,每次新增、修改或刪除資料後,我都得手動呼叫 ref.invalidateSelf() 來刷新 UI,不僅麻煩,還容易遺漏,導致畫面不同步。錯誤處理也需要自己額外補上邏輯,程式碼顯得冗長又分散。

完成改造之後,就可以快樂的全部砍掉~

UI 層的程式碼也完全不用動。只要透過 ref.watch() 監聽 Provider,UI 就能自動接收到來自資料庫的最新資料並立即刷新,完全省去手動更新的負擔。即便串流在運作過程中遇到錯誤,UI 也能自動捕捉並切換到錯誤狀態。

這讓架構層次分工更清晰:DAO 專注在資料庫操作,Notifier 負責協調並輸出即時資料流,UI 則單純專心呈現資料。從此以後,我能更安心地專注在產品邏輯與使用者體驗,而不是疲於處理畫面更新的瑣事。


上一篇
Day 19 - Drift 實戰:從卡頓到流暢,打造高效能的批次寫入術
系列文
《30 天 Flutter:跨平台 AI 行程規劃 App》20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言