前幾天在處理資料庫時,每當新增、更新或刪除資料後,我都得手動呼叫 ref.invalidateSelf()
,讓 Riverpod 重新計算 provider,進而刷新 UI。這種命令式(imperative)的寫法雖然有效,但不僅寫起來繁瑣,也容易因為忘記呼叫而出錯。
我開始思考,有沒有更「聰明」的方式?直到深入研究 Drift,才發現它提供了串流查詢(Stream Queries) 功能。我可以把查詢結果包裝成一個 Stream
,再透過 Riverpod 的 StreamProvider
綁定到 UI。這樣一來,只要資料庫有任何變動,串流就會自動發出最新結果,UI 也能立即收到更新,而不需要我再手動觸發。
今天,我就來分享這段從命令式到響應式的轉變歷程~
Stream
後,Drift 會自動監聽相關的資料表。只要有新增、更新或刪除發生,串流就會發出最新的查詢結果。若再搭配 Riverpod 的 StreamProvider
,UI 會自動收到狀態更新,開發者就不需要手動呼叫 ref.invalidateSelf()
。debounce
、throttle
或分頁等策略。為了將應用程式從手動更新的命令式寫法,徹底轉變為自動響應的串流架構,需要在三個主要環節進行調整:Drift DAO、Riverpod Notifier 和 Provider 宣告。
這是所有變動的起點。雖然 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()
。這是管理應用程式狀態的核心,需要將其從 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()
。最後一步,必須讓 Riverpod 知道現在管理的是一個「持續輸出的狀態」,而非一次性計算完成的狀態。
// trip_provider.dart
final tripListProvider = StreamNotifierProvider<TripListNotifier, List<Trip>>(
() => TripListNotifier(),
);
AsyncNotifierProvider
改為 StreamNotifierProvider
,StreamNotifierProvider
不會只計算一次結果,而是會持續監聽並輸出新的狀態。要不要我幫你再補一個「修改前 vs 修改後」的對照表?這樣讀者會一眼看懂從 Future
→ Stream
,以及 AsyncNotifier
→ StreamNotifier
的差別。
在這次轉換之前,每次新增、修改或刪除資料後,我都得手動呼叫 ref.invalidateSelf()
來刷新 UI,不僅麻煩,還容易遺漏,導致畫面不同步。錯誤處理也需要自己額外補上邏輯,程式碼顯得冗長又分散。
完成改造之後,就可以快樂的全部砍掉~
UI 層的程式碼也完全不用動。只要透過 ref.watch()
監聽 Provider,UI 就能自動接收到來自資料庫的最新資料並立即刷新,完全省去手動更新的負擔。即便串流在運作過程中遇到錯誤,UI 也能自動捕捉並切換到錯誤狀態。
這讓架構層次分工更清晰:DAO 專注在資料庫操作,Notifier 負責協調並輸出即時資料流,UI 則單純專心呈現資料。從此以後,我能更安心地專注在產品邏輯與使用者體驗,而不是疲於處理畫面更新的瑣事。