使用現代UI框架,最終目的雖然是要組建畫面,然而不要一開始就把頭埋入成千上萬的UI元件中,因為在摸索幾個基本UI元件之後,我們可以發現,框架如何管理狀態,才是最需要先行探索的重點之一!

下一步,狀態管理!

談到UI框架,往往會令我想起自己初學程式設計的年代。

相較於撰寫文字模式下的程式,具有圖形介面的程式很吸引人,就算在什麼都不懂的初學階段,用個VB拉個畫面,跑出個視窗程式,就令人興奮不已。後來有段時期我也著迷於GTK/Qt,以至於Java的AWT/Swing等,可以說我早期的程式設計基礎,都是在視窗程式的撰寫中奠定下來的。

然而隨著Web的興盛,越來越多的使用者介面搬到瀏覽器上,視窗程式的探討、文件、書籍漸漸少了,有一段時間我還覺得可惜,明明視窗程式中可以學到許多設計的概念,像是UI元件間的設計模式、職責分配方面的MVC等,我甚至覺得身為開發者,至少都該學點視窗程式設計呢!

不過,漸漸地,前端開發興起,我發現視窗程式的設計概念開始換了樣貌。

雖然早期也有ExtJS這類,在瀏覽器中模擬傳統視窗元件的框架,不過,視窗程式本質上就是使用者介面設計之一,瀏覽器中的使用者介面,並不止於模擬傳統視窗元件,因而瀏覽器開始也發展出屬於自己的一套設計哲學,從單純的DOM程式庫(jQuery)、後來的MVC、MVVM框架,仍至於虛擬DOM等,支援瀏覽器中實現使用者介面的相關技術,所涵蓋的概念也越來越多了。

這也就是我先前專欄〈漫談Flutter UI〉中談過的:面對UI框架時,若只關注於大量UI元件怎麼使用,就容易迷失於一個又一個範例。至少你必須先摸清楚UI元件的繼承架構等API設計,而這個建議之所以提出,其實也是來自於我過去研究視窗程式的心得。

那麼下一步呢?一起來研究UI框架如何支援狀態管理吧!

在先前專欄〈有限狀態機與前端〉我就談過,在Flux、Redux等架構提出後,狀態管理這件事在前端領域,成了熱門議題之一,有好的狀態管理,才容易控制狀態的轉移,也就易於推導應用程式目前處於何種狀態。

Widget=build(state)

重視狀態管理的這股風潮,不單只發生於瀏覽器中的使用者介面,如今也吹到了手機/平板的App開發領域。

姑且不論React在App這方面就有React Native這門技術,我近來在研究Flutter時發現:是否掌握狀態管理,對於能否善用Flutter,往往扮演很重要的角色。

〈漫談Flutter UI〉中談到,開發者使用Flutter撰寫程式建構畫面時,幾乎都是跟Widget打交道,在組建畫面時,最常關心的是build方法怎麼寫,對於StatelessWidget,它的狀態不會改變,想改變StatelessWidget的屬性,就是指定新屬性「重新建立」Widget。

那麼StatefulWidget呢?嚴格來說,StatefulWidget實例本身狀態也是不會改變的,狀態會變化的其實是createState傳回的State實例,而State的build方法,會根據State當前的狀態「重新建立」Widget。

StatelessWidget的屬性也是一種狀態,就這點來看,無論是StatelessWidget或StatefulWidget,都是根據目前狀態重新建立Widget,Widget=build(state),你改變了狀態,對應的UI就會重建,Widget就是UI,build就是函式,也就是UI=f(state),狀態是輸入,UI是結果-宣告式的UI風格。

因為StatelessWidget狀態不會改變,新Widget的產生來源,主要就是StatefulWidget了,對應的State若以setState通知框架狀態發生變化,框架就會呼叫build建立新Widget,Widget樹就會變化,然而Element不見得!

雖然開發者多半與Widget打交道,不過,在Widget的API文件上的第一句話,就說明它是用來「描述Element的組態」,Element會由Flutter管理,開發者幾乎不會去干涉Element,然而在Flutter中想掌握狀態,就一定要認識Element。

Widget樹與Element樹

簡單來說,Widget與Element是一對多的關係。Widget只是組態,可以放到Widget樹的多個位置,也可以從Widget取下;每次Widget被放到Widget樹上,就會生成一個Element,因此,如果一個Widget被用於Widget樹多個位置,就會生成多個Element;而Element也會組成一棵樹,Element類別的定義,主要都是與Element樹的維護相關。

Widget樹與Element樹有結構上的對應,Element會參考當初建立它的Widget。如果Widget樹有了變化,Element會對持有的Widget與新的Widget,進行比較。

具體來說,這是透過由Widget的canUpdate靜態方法決定,如果canUpdate傳回true,既有的Element實例會參考至新的Widget,而不是建立新的Element實例,因此build建立了新Widget,Element樹不見得會改變。

StatelessElement所對應的是StatelessWidget,StatelessWidget實例本身就有建立畫面的相關屬性(狀態),而StatelessElement單純更新參考的Widget,基本上沒有問題(而且經濟),但是,面對StatefulWidget就要小心了,因為,StatefulElement才會持有State物件(也就是擁有狀態),StatefulElement若單純更新參考的Widget,原本持有的State物件會維持不變。

〈Keys! What are they good for?〉就舉了個實際案例。兩個StatefulWidget的位置交換了,卻沒有產生對應的顏色變化,而解決的方式,是透過key特性的設置,令canUpdate傳回false,不讓Element單純更新Widget,而是令Element樹也做出位置上的調整。

所以,我們要注意的,不單只是Element是否會更新Widget的問題。因為,每次Widget被放到Widget樹上,就會生成一個Element。而這也意味著,對於StatefulWidget來說,createState會被多次呼叫,表示狀態會被重新建立,例如,如果把同一StatefulWidget實例從Widget樹拿掉後又放上,就會失去先前的狀態。

若要解決這樣的問題,處理的方式視需求而定,基本上,我們可考量狀態管理由哪個元件來做:子元件?父元件?或是兩者之混合?對此,在〈Managing state〉中,有些範例可以參考。

框架如何管理狀態?

在各式的狀態管理討論上,Flutter的官方文件也花了不少篇幅說明,例如〈State management〉就提供了不少文件資源,其中,對於短時(ephemeral)狀態與App狀態的區分,也值得思考。

不只是Flutter,對於現代UI框架來說,講再多的狀態管理也不為過,開發者應該將UI框架對狀態管理的設計與支援方式,列為接觸UI框架時的首要課題之一,甚至去試著認識支援UI框架的第三方狀態管理程式庫,從中知道UI框架本身可能有哪些不足之處,以及其他可行的方案。

作者簡介


Advertisement

更多 iThome相關內容