← Flutter 開發設計雜談

Day 11 - 四處可見地倒數計時

有時候,我們需要根據時間來渲染畫面,並時時刻刻的更新。一個經典的例子就是顯示倒數時間,例如:商品打折的剩餘時間,比賽開始的倒數時間 …等等。我們使用最簡單的 StatefulWidget + Timer 來完成第一版的程式碼。

Article image

https://dartpad.dev/?id=95c6e97373a5fce940e29b336cb57057

運行上面的程式碼之後,可以發現程式如我們預期的一秒一秒倒數。如果今天畫面上只有一個倒數時間,我們就可以收工下班了。但是,有些時候畫面可能會同時出多個倒數時間,此時我們會發現一些問題。

當畫面上有多個倒數時間時

當我們滑動畫面,就會發現畫面的倒數時間跳動,變得非常不整齊。原因是每個 CountdownWidget 建立的時間不一致,導致每個 Widget 的 Timer 之間的更新頻率無法對齊,時間更新也就變得不整齊。

Article image

https://dartpad.dev/?id=f163e5e37cf44378aacfc1fc133079e7

Global Timer

為了解決這個問題,最簡單的方式是將 Timer 移出 Widget,以全域變數的形式存在程式中。在 Timer 中,我們使用觀察者模式,讓 Widget 向 Timer 註冊與監聽時間的跳動,並在接收到通知時更新畫面。這樣一來,Widget 能盡量在相同的時間進行時間跳動。

Article image

https://dartpad.dev/?id=e031a0ec59b156c3e176b68b60c2d6ce

在這個作法中,Timer 必須維護 Observer 列表,而 Widget 本身也需要自己處理 Observer 的註冊與註銷。假設我們今天不使用 Flutter 而是單純的使用 Dart 來開發其他應用,這個作法並不算太差。但假設我們是使用 Flutter 的話,我們可以利用 Flutter 的框架機制,讓我們少維護一些程式碼。

InheritedWidget / Provider

在前幾天的文章中,我們討論到使用 InheritedWidget / Provider 來共享參數。同樣的,我們也可以透過 InheritedWidget / Provider 來共享 Timer,當更新時間到了的時候,透過 InheritedWidget / Provider 幫助我們更新所有倒數計時的畫面。

在這邊我們使用 Provider 來改寫上面的例子,在 CountdownWidget 中,使用 context.watch 讀取並監聽變化。當更新時間到了的時候,Provider 會透過 notifyListeners 通知所有畫面。

Article image

https://dartpad.dev/?id=82d7e6a3cc2e2ede3106138f6c3401c8

使用這個作法,我們可以省去維護自己的 Observer List,Widget 也能使用 StatelessWidget 就好,讓程式碼變得更簡潔。

無論是 GlobalTimer 作法或 InheritedWidget / Provider 作法,都有一個明顯的問題:那就是 CountdownWidget 必須依賴於 CountdownTimer 才能工作,每當我想要使用 CountdownWidget 時,想辦法提供他 CountdownTimer。

Article image

使用 Ticker

大家如果有在 Flutter 中使用過 Tab 的話,肯定對 Ticker 不陌生,使用 TabController 時,需要傳一個 vsync,而 vsync 的其實就是 TickerProvider。Ticker 人如其名,讓我們就像時鐘一樣滴搭滴搭的執行某個方法,讓我們使用 Ticker 來改寫倒數計時吧。

Article image

https://dartpad.dev/?id=7fd27c9317101180fb3b8fff201511ca

使用 Ticker 的話,我們可以解決時間同步的問題,也不需要把 Timer 往 Widget 外搬。用法上,與 Timer 一樣,需要在 initState / dispose 處理 Ticker 的生命週期,卻不會有 Timer 更新不同步的問題。相比於 Global 與 InheritedWidget / Provider 作法,使用 Ticker 能提升 Widget 的內聚力,自己就可以完成所有事情,不需要靠外部的物件來協助自己更新畫面。

結論

如果今天我們的需求只是倒數計時,那我們考慮優先使用 Ticker 解決問題。當未來倒數可能不只是倒數,需要加上一些商業邏輯時,使用 InheritedWidget / Provider 或其他狀態管理套件就比較合適。無論什麼作法,都需要優先考慮需求與情境,其次才是討論什麼是最適合的作法,讓開發與維護變得更容易。