深入理解Flutter UI系統

會飛的狼發表於2020-07-17

Flutter UI系統

它提供了一套Dart API,然後在底層通過OpenGL這種跨平臺的繪製庫(內部會呼叫作業系統API)實現了一套程式碼跨多端。由於Dart API也是呼叫作業系統API,所以它的效能接近原生。

Flutter中,一切都是Widget,當UI要發生變化時,我們不去直接修改DOM,而是通過更新狀態,讓Flutter UI系統來根據新的狀態來重新構建UI。

Widget與Element

Widget只是UI元素的一個配置資料,並且一個Widget可以對應多個Element

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;

  @protected
  Element createElement();

  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}
複製程式碼
  • canUpdate(...)是一個靜態方法,它主要用於在Widget樹重新build時複用舊的widget,其實具體來說,應該是:是否用新的Widget物件去更新舊UI樹上所對應的Element物件的配置;通過其原始碼我們可以看到,只要newWidgetoldWidgetruntimeTypekey同時相等時就會用newWidget去更新Element物件的配置,否則就會建立新的Element

另外Widget類本身是一個抽象類,其中最核心的就是定義了createElement()介面,在Flutter開發中,我們一般都不用直接繼承Widget類來實現一個新元件,相反,我們通常會通過繼承StatelessWidgetStatefulWidget來間接繼承Widget類來實現。StatelessWidgetStatefulWidget都是直接繼承自Widget類,而這兩個類也正是Flutter中非常重要的兩個抽象類,它們引入了兩種Widget模型,接下來我們將重點介紹一下這兩個類。

3.1.4 StatelessWidget

StatelessWidget用於不需要維護狀態的場景,它通常在build方法中通過巢狀其它Widget來構建UI,在構建過程中會遞迴的構建其巢狀的Widget。我們看一個簡單的例子:

Context

build方法有一個context引數,它是BuildContext類的一個例項,表示當前widget在widget樹中的上下文,每一個widget都會對應一個context物件(因為每一個widget都是widget樹上的一個節點)。實際上,context是當前widget在widget樹中位置中執行”相關操作“的一個控制程式碼,比如它提供了從當前widget開始向上遍歷widget樹以及按照widget型別查詢父級widget的方法。下面是在子樹中獲取父級widget的一個示例:

lass ContextRoute extends StatelessWidget {
  @override
 
 Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context測試"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在Widget樹中向上查詢最近的父級`Scaffold` widget
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title, 此處實際上是Text("Context測試")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}
複製程式碼

3.1.5 StatefulWidget

StatelessWidget一樣,StatefulWidget也是繼承自Widget類,並重寫了createElement()方法,不同的是返回的Element 物件並不相同;另外StatefulWidget類中新增了一個新的介面createState()

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);

  @override
  StatefulElement createElement() => new StatefulElement(this);

  @protected
  State createState();
}

複製程式碼
  • StatefulElement 間接繼承自Element類,與StatefulWidget相對應(作為其配置資料)。StatefulElement中可能會多次呼叫createState()來建立狀態(State)物件。
  • createState() 用於建立和Stateful widget相關的狀態,它在Stateful widget的生命週期中可能會被多次呼叫。例如,當一個Stateful widget同時插入到widget樹的多個位置時,Flutter framework就會呼叫該方法為每一個位置生成一個獨立的State例項,其實,本質上就是一個StatefulElement對應一個State例項。

3.1.6 State

在widget生命週期中可以被改變,當State被改變時,可以手動呼叫其setState()方法通知Flutter framework狀態發生改變,Flutter framework在收到訊息後,會重新呼叫其build方法重新構建widget樹,從而達到更新UI的目的。

State生命週期

  void initState()
  Widget build(BuildContext context) 
  void didUpdateWidget(CounterWidget oldWidget) 
  void deactivate() 
  void dispose() 
  void didChangeDependencies() 

複製程式碼

我們執行應用並開啟該路由頁面,在新路由頁開啟後

I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build
複製程式碼

可以看到,在StatefulWidget插入到Widget樹時首先initState方法會被呼叫。

然後我們點選⚡️按鈕熱過載,控制檯輸出日誌如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build
複製程式碼

可以看到此時initStatedidChangeDependencies都沒有被呼叫,而此時didUpdateWidget被呼叫。

  • initState:當Widget第一次插入到Widget樹時會被呼叫,對於每一個State物件,Flutter framework只會呼叫一次該回撥,所以,通常在該回撥中做一些一次性的操作,如狀態初始化、訂閱子樹的事件通知等。不能在該回撥中呼叫BuildContext.dependOnInheritedWidgetOfExactType(該方法用於在Widget樹上獲取離當前widget最近的一個父級InheritFromWidget,關於InheritedWidget我們將在後面章節介紹),原因是在初始化完成後,Widget樹中的InheritFromWidget也可能會發生變化,所以正確的做法應該在在build()方法或didChangeDependencies()中呼叫它。
  • didChangeDependencies():當State物件的依賴發生變化時會被呼叫;例如:在之前build() 中包含了一個InheritedWidget,然後在之後的build()InheritedWidget發生了變化,那麼此時InheritedWidget的子widget的didChangeDependencies()回撥都會被呼叫。典型的場景是當系統語言Locale或應用主題改變時,Flutter framework會通知widget呼叫此回撥。
  • build():此回撥讀者現在應該已經相當熟悉了,它主要是用於構建Widget子樹的,會在如下場景被呼叫:
    1. 在呼叫initState()之後。
    2. 在呼叫didUpdateWidget()之後。
    3. 在呼叫setState()之後。
    4. 在呼叫didChangeDependencies()之後。
    5. 在State物件從樹中一個位置移除後(會呼叫deactivate)又重新插入到樹的其它位置之後。
  • reassemble():此回撥是專門為了開發除錯而提供的,在熱過載(hot reload)時會被呼叫,此回撥在Release模式下永遠不會被呼叫。
  • didUpdateWidget():在widget重新構建時,Flutter framework會呼叫Widget.canUpdate來檢測Widget樹中同一位置的新舊節點,然後決定是否需要更新,如果Widget.canUpdate返回true則會呼叫此回撥。正如之前所述,Widget.canUpdate會在新舊widget的key和runtimeType同時相等時會返回true,也就是說在在新舊widget的key和runtimeType同時相等時didUpdateWidget()就會被呼叫。
  • deactivate():當State物件從樹中被移除時,會呼叫此回撥。在一些場景下,Flutter framework會將State物件重新插到樹中,如包含此State物件的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey來實現)。如果移除後沒有重新插入到樹中則緊接著會呼叫dispose()方法。
  • dispose():當State物件從樹中被永久移除時呼叫;通常在此回撥中釋放資源。

StatefulWidget生命週期如圖3-2所示:

圖3-2

3.1.8 Flutter SDK內建元件庫介紹

Flutter提供了一套豐富、強大的基礎元件,在基礎元件庫之上Flutter又提供了一套Material風格(Android預設的視覺風格)和一套Cupertino風格(iOS視覺風格)的元件庫。要使用基礎元件庫,需要先匯入:

import 'package:flutter/widgets.dart';
複製程式碼

下面我們介紹一下常用的元件。

基礎元件

  • Text:該元件可讓您建立一個帶格式的文字。
  • RowColumn: 這些具有彈性空間的佈局類Widget可讓您在水平(Row)和垂直(Column)方向上建立靈活的佈局。其設計是基於Web開發中的Flexbox佈局模型。
  • Stack: 取代線性佈局 (譯者語:和Android中的FrameLayout相似),Stack允許子 widget 堆疊, 你可以使用 Positioned 來定位他們相對於Stack的上下左右四條邊的位置。Stacks是基於Web開發中的絕對定位(absolute positioning )佈局模型設計的。
  • ContainerContainer 可讓您建立矩形視覺元素。container 可以裝飾一個BoxDecoration, 如 background、一個邊框、或者一個陰影。 Container 也可以具有邊距(margins)、填充(padding)和應用於其大小的約束(constraints)。另外, Container可以使用矩陣在三維空間中對其進行變換。

Material元件

Flutter提供了一套豐富的Material元件,它可以幫助我們構建遵循Material Design設計規範的應用程式。Material應用程式以MaterialApp 元件開始, 該元件在應用程式的根部建立了一些必要的元件,比如Theme元件,它用於配置應用的主題。 是否使用MaterialApp完全是可選的,但是使用它是一個很好的做法。在之前的示例中,我們已經使用過多個Material 元件了,如:ScaffoldAppBarFlatButton等。要使用Material 元件,需要先引入它:

import 'package:flutter/material.dart';
複製程式碼

Cupertino元件

Flutter也提供了一套豐富的Cupertino風格的元件,儘管目前還沒有Material 元件那麼豐富,但是它仍在不斷的完善中。值得一提的是在Material 元件庫中有一些元件可以根據實際執行平臺來切換表現風格,比如MaterialPageRoute,在路由切換時,如果是Android系統,它將會使用Android系統預設的頁面切換動畫(從底向上);如果是iOS系統,它會使用iOS系統預設的頁面切換動畫(從右向左)。由於在前面的示例中還沒有Cupertino元件的示例,下面我們實現一個簡單的Cupertino元件風格的頁面:

//匯入cupertino widget庫
import 'package:flutter/cupertino.dart';

class CupertinoTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text("Cupertino Demo"),
      ),
      child: Center(
        child: CupertinoButton(
            color: CupertinoColors.activeBlue,
            child: Text("Press"),
            onPressed: () {}
        ),
      ),
    );
  }
}
複製程式碼

其他

  1. 深入理解StatelessWidget

  2. abstract class StatelessWidget extends Widget Containing class: StatelessWidget
    A widget that does not require mutable state. A stateless widget is a widget that describes part of the user interface by building a constellation of other widgets that describe the user interface more concretely. The building process continues recursively until the description of the user interface is fully concrete (e.g., consists entirely of RenderObjectWidgets, which describe concrete RenderObjects). www.youtube.com/watch?v=wE7khGHVkYY Stateless widget are useful when the part of the user interface you are describing does not depend on anything other than the configuration information in the object itself and the BuildContext in which the widget is inflated. For compositions that can change dynamically, e.g. due to having an internal clock-driven state, or depending on some system state, consider using StatefulWidget. Performance considerations The build method of a stateless widget is typically only called in three situations: the first time the widget is inserted in the tree, when the widget's parent changes its configuration, and when an InheritedWidget it depends on changes. If a widget's parent will regularly change the widget's configuration, or if it depends on inherited widgets that frequently change, then it is important to optimize the performance of the build method to maintain a fluid rendering performance. There are several techniques one can use to minimize the impact of rebuilding a stateless widget: Minimize the number of nodes transitively created by the build method and any widgets it creates. For example, instead of an elaborate arrangement of Rows, Columns, Paddings, and SizedBoxes to position a single child in a particularly fancy manner, consider using just an Align or a CustomSingleChildLayout. Instead of an intricate layering of multiple Containers and with Decorations to draw just the right graphical effect, consider a single CustomPaint widget. Use const widgets where possible, and provide a const constructor for the widget so that users of the widget can also do so. Consider refactoring the stateless widget into a stateful widget so that it can use some of the techniques described at StatefulWidget, such as caching common parts of subtrees and using GlobalKeys when changing the tree structure. If the widget is likely to get rebuilt frequently due to the use of InheritedWidgets, consider refactoring the stateless widget into multiple widgets, with the parts of the tree that change being pushed to the leaves. For example instead of building a tree with four widgets, the inner-most widget depending on the Theme, consider factoring out the part of the build function that builds the inner-most widget into its own widget, so that only the inner-most widget needs to be rebuilt when the theme changes. {@tool snippet} The following is a skeleton of a stateless widget subclass called GreenFrog. Normally, widgets have more constructor arguments, each of which corresponds to a final property. class GreenFrog extends StatelessWidget { const GreenFrog({ Key key }) : super(key: key);

    @override Widget build(BuildContext context) { return Container(color: const Color(0xFF2DBD3A)); } } {@end-tool} {@tool snippet} This next example shows the more generic widget Frog which can be given a color and a child: class Frog extends StatelessWidget { const Frog({ Key key, this.color = const Color(0xFF2DBD3A), this.child, }) : super(key: key);

    final Color color; final Widget child;

    @override Widget build(BuildContext context) { return Container(color: color, child: child); } } {@end-tool} By convention, widget constructors only use named arguments. Named arguments can be marked as required using @required. Also by convention, the first argument is key, and the last argument is child, children, or the equivalent. See also: StatefulWidget and State, for widgets that can build differently several times over their lifetime. InheritedWidget, for widgets that introduce ambient state that can be read by descendant widgets.

    1. 不需要可變狀態的小部件。
    2. 非常具體描述某一個事情的時候使用
    3. 參考: www.youtube.com/watch?v=wE7khGHVkYY
    4. 當您所描述的使用者介面部分不依賴於物件本身中的配置資訊和小部件膨脹的構建上下文之外的任何東西時,無狀態小部件非常有用。對於可以動態更改的組合,例如,由於具有內部時鐘驅動狀態,或取決於某些系統狀態,請考慮使用StatefulWidget。
    5. 效能注意事項
      1. 無狀態小部件的構建方法通常只在三種情況下呼叫:
      2. 第一次將小部件插入樹中時,
      3. 當小部件的父級更改其配置時,
      4. 以及當繼承的小部件依賴於更改時。
    6. 如果小部件的父級將定期更改小部件的配置,或者如果它依賴於頻繁更改的繼承小部件,那麼優化構建方法的效能以保持流體渲染效能非常重要。
      1. 最小化由build方法及其建立的任何小部件可傳遞地建立的節點數。例如,為了以一種特別奇特的方式定位單個子級,而不是對行、列、填充和大小框進行精心安排,而是考慮使用Align或CustomSingleChildLayout。考慮一個單獨的CustomPaint小部件,而不是用多個容器和裝飾來繪製正確的圖形效果的複雜分層。
      2. 儘可能使用const widgets,併為小部件提供一個const建構函式,這樣小部件的使用者也可以這樣做。
      3. 考慮將無狀態小部件重構為有狀態小部件,以便它可以使用StatefulWidget中描述的一些技術,例如快取子樹的公共部分,以及在更改樹結構時使用globalkey。
      4. 如果小部件可能由於使用繼承的小部件而頻繁地重新構建,那麼考慮將無狀態的小部件重構為多個小部件,並將樹中更改的部分推到葉子上。例如,與其構建一個包含四個小部件(最內部的小部件取決於主題)的樹,不如考慮將構建最內部小部件的構建函式部分分解到自己的小部件中,這樣當主題發生變化時,只需要重新構建最內部的小部件。
  3. StatelessWidget

  4. InheritedWidget 資料共享元件

    1. 提供了一種資料在widget樹中從上到下傳遞、共享的方式,比如我們在應用的根widget中通過InheritedWidget共享了一個資料,那麼我們便可以在任意子widget中來獲取該共享的資料!如Flutter SDK中正是通過InheritedWidget來共享應用主題(Theme)和Locale (當前語言環境)資訊的。

    2. 特性 didChangeDependencies

    3. state中didChangeDependencies的回撥方法,再依賴發生變化的時候會進行被呼叫;依賴的行為只指子的widget是否使用了父widget中InheritedWidget 的資料;如果使用了就代表依賴了,依賴其的子widget的didChangeDependencies方法將會被呼叫。

    4. context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget
      
      複製程式碼
    5. 如果是通過 'getElementForInheritedWidgetOfExactType'

    6. 什麼時候使用

      1. 子元件共享獲取全域性共享資料時候,比如主題變化

相關文章