Widget 中的 State 解析

宋魚000發表於2019-09-26

StatefulWidget 應對有互動、需要動態變化視覺效果的場景

StatelessWidget 則用於處理靜態的、無狀態的檢視展示

那麼,StatelessWidget 是否有存在的必要?StatefulWidget 是否是 Flutter 中的萬金油?

UI 程式設計正規化

在 Flutter 中,如何調整一個控制元件(Widget)的展示樣式,即 UI 程式設計正規化。

原生系統(Android、iOS)或原生 JavaScript 開發中,檢視開發是命令式的,需要精確地告訴作業系統或瀏覽器用何種方式去做事情。比如,如果想要變更介面的某個文案,則需要找到具體的文字控制元件並呼叫它的控制元件方法命令,才能完成文字變更。

下述程式碼分別展示在 Android、iOS 及原生 Javascript 中,如何將一個文字控制元件的展示文案更改為 Hello World:

// Android 設定某文字控制元件展示文案為 Hello World
TextView textView = (TextView) findViewById(R.id.txt);
textView.setText("Hello World");

// iOS 設定某文字控制元件展示文案為 Hello World
UILabel *label = (UILabel *)[self.view viewWithTag:1234];
label.text = @"Hello World";

// 原生 JavaScript 設定某文字控制元件展示文案為 Hello World
document.querySelector("#demo").innerHTML = "Hello World!";
複製程式碼

與此不同的是,Flutter 的檢視開發是宣告式的,其核心設計思想就是將檢視和資料分離,這與 React 的設計思路完全一致

Flutter 要實現同樣的需求,則要麻煩點:除了設計好 Widget 佈局方案之外,還需要提前維護一套文案資料集,併為需要變化的 Widget 繫結集中的資料,使 Widget 根據這個資料集完成渲染。

但是,當需要變更介面的文案時,只要改變資料集中的文案資料,並通知 Flutter 框架觸發 Widget 的重新渲染即可。比起命令式的檢視開發方式需要挨個設定不同組建(Widget)的視覺屬性,這種方式要便捷得多。

總結來說,指令式程式設計強調精確控制過程細節,而宣告式程式設計強調通過意圖輸出結果整體。對應到 Flutter 中,意圖是繫結來元件狀態的 State,結果則是重新渲染後的元件。在 Widget 的宣告週期內,應用到 State 中的任何更改都將強制 Widget 重新構建。

其中,對於元件完成建立後就無需變更的場景,狀態的繫結是可選項。這裡的“可選”就區分出了 Widget 的兩種型別,即:StatelessWidget 不帶繫結狀態,而 StatefulWidget 帶繫結狀態。當所要構建的使用者介面不隨任何狀態資訊的變化而變化時,需要選擇使用 StatelessWidget,反之則選用 StatefulWidget。前者一般用於靜態內容的展示,而後者則用於存在互動反饋的內容呈現中。

StatelessWidget

在 Flutter 中,Widget 採用由父到子、自頂向下的方式進行構建,父 Widget 控制著 Widget 的顯示樣式,其樣式配置由父 Widget 在構建時提供。

用如上方式構建出的 Widget,有些(比如 Text、Container、Row、Column 等)在建立時,除了這些配置引數之外不依賴於任何其他資訊,換句話說,它們一旦建立成功就不再關心、也不響應任何資料變化進行重繪。在 Flutter 中,這樣的 Widget 被稱為 StatelessWidget(無狀態元件)

以 Text 的部分原始碼為例,說明 StatelessWidget 的構建過程。
class Text extends StatelessWidget {
  
  // 構造方法及屬性宣告部分
  const Text(this.data, {
    key key,
    this.textAlign,
    this.textDirection,
    // 其他引數
    ...
  }) : assert(data != null),
    textSpan = null,
    super(key: key);
  
  final string data;
  final TextAlign textAlign;
  final TextDirection textDirection;
  // 其他屬性
  ...
  
  @override
  Widget build(BuildContext context) {
    ... 
    Widget result = RichText(
      // 初始化配置
      ... 
    );
    ...
    return result;
  }
}
複製程式碼

程式碼中可以看到,在構造方法將其屬性列表賦值後,build 方法隨即將子元件 RichText 通過其屬性列表(如文字 data、對齊方式 textAlign、文字展示方向 textDirection 等)初始化後返回,之後 Text 內部不再響應外部資料的變化。

那麼,什麼應用場景下應該使用 StatelessWidget 呢?

簡單的判斷規則:父 Widget 是否能夠通過初始化引數完全控制其 UI 展示效果? 如果能,那麼就可以使用 StatelessWidget 來設計建構函式介面了。

比如錯誤資訊提示的彈框控制元件,可以採用繼承 StatelessWidget 的方式,來進行元件自定義。計數器按鈕需要通過繼承 StatefulWidget 的方式,來進行元件自定義。

StatefulWidget

有一些 Widget(比如 Image、Checkbox)的展示,除了父 Widget 初始化時傳入的靜態配置之外,還需要處理使用者的互動(比如,使用者點選按鈕)或其內部資料的變化(比如,網路資料回包),並體現在 UI 上。換句話說,這些 Widget 建立完成後,還需要關心和響應資料變化來進行重繪。在 Flutter 中,這一類 Widget 被稱為 StatefulWidget(有狀態元件)

StatefulWidget 是以 State 類代理 Widget 構建的設計方式實現的。接下來,以 Image 的部分原始碼為例,說明 StatefulWidget 的構造過程。

和上面的 Text 一樣,Image 類的建構函式會接收要被這個類使用的屬性引數。然而,不同的是,Image 類並沒有 build 方法來建立檢視,而是通過 createState 方法建立來一個型別為 _ImageState 的 state 物件,然後由這個物件負責檢視的構建。

這個 state 物件持有並處理來 Image 類中的狀態變化,所以就以 _imageInfo 屬性為例來展開說明。

_imageInfo 屬性用來給 Widget 載入真實的圖片,一旦 State 物件通過 _handleImageChanged 方法監聽到 _imageInfo 屬性發生來變化,就會立即呼叫 _ImageStage 類的 setState 方法通知 Flutter 框架:“資料變了,請使用更新後的 _imageInfo 資料重新載入圖片!”。Flutter 框架則會標記檢視狀態,更新 UI。

StatefulWidget 不是萬金油,要慎用

事實上,StatefulWidget 的濫用會直接影響 Flutter 應用的渲染效能。

Widget 是不可變的,更新則意味著銷燬 + 重建(build)。StatelessWidget 是靜態的,一旦建立則無需更新;而對於 StateFulWidget 來說,在 State 類中呼叫 setState 方法更新資料,會觸發檢視的銷燬和重建,也將間接地觸發其每個子 Widget 的銷燬和重建。

如果根佈局是一個 StatefulWidget,在其 State 中每呼叫一次更新 UI,都將是一整個頁面所有 Widget 的銷燬和重建。

雖然 Flutter 內部通過 Element 層可以最大程度地降低對真實渲染檢視的修改,提高渲染效率,而不是銷燬整個 RenderObject 樹重建。但,大量 Widget 物件的銷燬重建是無法避免的。如果某個子 Widget 的重建涉及到一些耗時操作,那頁面的渲染效能將會急劇下降。

因此,正確評估檢視展示需求,避免無謂的 StatefulWidget 使用,是提高 Flutter 應用渲染效能最簡單也是最直接的手段

相關文章