有時候,我們需要根據時間來渲染畫面,並時時刻刻的更新。一個經典的例子就是顯示倒數時間,例如:商品打折的剩餘時間,比賽開始的倒數時間 …等等。我們使用最簡單的 StatefulWidget + Timer 來完成第一版的程式碼。
https://dartpad.dev/?id=95c6e97373a5fce940e29b336cb57057
運行上面的程式碼之後,可以發現程式如我們預期的一秒一秒倒數。如果今天畫面上只有一個倒數時間,我們就可以收工下班了。但是,有些時候畫面可能會同時出多個倒數時間,此時我們會發現一些問題。
當我們滑動畫面,就會發現畫面的倒數時間跳動,變得非常不整齊。原因是每個 CountdownWidget 建立的時間不一致,導致每個 Widget 的 Timer 之間的更新頻率無法對齊,時間更新也就變得不整齊。
https://dartpad.dev/?id=f163e5e37cf44378aacfc1fc133079e7
為了解決這個問題,最簡單的方式是將 Timer 移出 Widget,以全域變數的形式存在程式中。在 Timer 中,我們使用觀察者模式,讓 Widget 向 Timer 註冊與監聽時間的跳動,並在接收到通知時更新畫面。這樣一來,Widget 能盡量在相同的時間進行時間跳動。
https://dartpad.dev/?id=e031a0ec59b156c3e176b68b60c2d6ce
在這個作法中,Timer 必須維護 Observer 列表,而 Widget 本身也需要自己處理 Observer 的註冊與註銷。假設我們今天不使用 Flutter 而是單純的使用 Dart 來開發其他應用,這個作法並不算太差。但假設我們是使用 Flutter 的話,我們可以利用 Flutter 的框架機制,讓我們少維護一些程式碼。
在前幾天的文章中,我們討論到使用 InheritedWidget / Provider 來共享參數。同樣的,我們也可以透過 InheritedWidget / Provider 來共享 Timer,當更新時間到了的時候,透過 InheritedWidget / Provider 幫助我們更新所有倒數計時的畫面。
在這邊我們使用 Provider 來改寫上面的例子,在 CountdownWidget 中,使用 context.watch 讀取並監聽變化。當更新時間到了的時候,Provider 會透過 notifyListeners 通知所有畫面。
https://dartpad.dev/?id=82d7e6a3cc2e2ede3106138f6c3401c8
使用這個作法,我們可以省去維護自己的 Observer List,Widget 也能使用 StatelessWidget 就好,讓程式碼變得更簡潔。
無論是 GlobalTimer 作法或 InheritedWidget / Provider 作法,都有一個明顯的問題:那就是 CountdownWidget 必須依賴於 CountdownTimer 才能工作,每當我想要使用 CountdownWidget 時,想辦法提供他 CountdownTimer。
大家如果有在 Flutter 中使用過 Tab 的話,肯定對 Ticker 不陌生,使用 TabController 時,需要傳一個 vsync,而 vsync 的其實就是 TickerProvider。Ticker 人如其名,讓我們就像時鐘一樣滴搭滴搭的執行某個方法,讓我們使用 Ticker 來改寫倒數計時吧。
https://dartpad.dev/?id=7fd27c9317101180fb3b8fff201511ca
使用 Ticker 的話,我們可以解決時間同步的問題,也不需要把 Timer 往 Widget 外搬。用法上,與 Timer 一樣,需要在 initState / dispose 處理 Ticker 的生命週期,卻不會有 Timer 更新不同步的問題。相比於 Global 與 InheritedWidget / Provider 作法,使用 Ticker 能提升 Widget 的內聚力,自己就可以完成所有事情,不需要靠外部的物件來協助自己更新畫面。
如果今天我們的需求只是倒數計時,那我們考慮優先使用 Ticker 解決問題。當未來倒數可能不只是倒數,需要加上一些商業邏輯時,使用 InheritedWidget / Provider 或其他狀態管理套件就比較合適。無論什麼作法,都需要優先考慮需求與情境,其次才是討論什麼是最適合的作法,讓開發與維護變得更容易。