Flutter開發之基礎Widgets

Widgets概念

Flutter裏有一個非常重要的核心理念:一切皆爲組件,Flutter的所有元素都是由控件構成的。
與原生開發中控件所代表的含義不同,Flutter中widget的概念更加廣泛,它不僅可以表示UI元素,也可以表示一些功能性的組件,如用於手勢檢測的 GestureDetector widget、用於應用主題數據傳遞的Theme等等。而原生開發中的控件通常只是指UI元素。由於Flutter主要就是用於構建用戶界面的,所以,在大多數時候,我們可以簡單的認爲widget就是一個控件,不必糾結於概念。

Widget與Element

在正式介紹Flutter的Widget之前,我們需要理清兩個概念,即什麼是Widget,什麼是Element?

Widget的功能是“描述一個UI元素的配置數據,它就是說,Widget其實並不是表示最終繪製在設備屏幕上的顯示元素,而只是顯示元素的一個配置數據。實際上,Flutter中真正代表屏幕上顯示元素的類是Element,也就是說Widget只是描述Element的一個配置。並且一個Widget可以對應多個Element,這是因爲同一個Widget對象可以被添加到UI樹的不同部分,而真正渲染時,UI樹的每一個Widget節點都會對應一個Element對象。所以,理解Flutter的Widget需要理清兩個概念:

  • Widget實際上就是Element的配置數據, Widget的功能是描述一個UI元素的一個配置數據, 而真正的UI渲染是由Element構成的。
  • 由於Element是通過Widget生成,所以它們之間有對應關係,所以在大多數場景,我們可以簡單地認爲Widget就是指UI控件或UI渲染。

Widget聲明

首先,我們先來看一下Widget類的聲明:

@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;
  }
}

從這個Widget類的申明中,我們可以得到如下一些信息:

  • Widget類繼承自DiagnosticableTree,主要作用是提供調試信息。
  • Key: 這個key屬性類似於React/Vue中的key,主要的作用是決定是否在下一次build時複用舊的widget,決定的條件在canUpdate()方法中
  • createElement():正如前文所述一個Widget可以對應多個Element;Flutter Framework在構建UI時,會先調用此方法生成對應節點的Element對象。此方法是Flutter Framework隱式調用的,在我們開發過程中基本不會調用到。
  • debugFillProperties 複寫父類的方法,主要是設置DiagnosticableTree的一些特性。
  • canUpdate()是一個靜態方法,它主要用於在Widget樹重新build時複用舊的widget。具體來說,是否使用新的Widget對象去更新舊UI樹上所對應的Element對象的配置;並且通過其源碼我們可以知道,只要newWidget與oldWidget的runtimeType和key同時相等時就會用newWidget去更新Element對象的配置,否則就會創建新的Element。

StatelessWidget

StatelessWidget是Flutter提供的一個不需要狀態更改的widget ,它沒有內部狀態管理功能。StatelessWidget相對比較簡單,它繼承自Widget類,重寫了createElement()方法。

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

StatelessElement 間接繼承自Element類,與StatelessWidget相對應。StatelessWidget通常被用於不需要維護狀態的場景,在build方法中通過嵌套其它Widget來構建UI,在構建過程中會遞歸的構建其嵌套的Widget。例如:

class Echo extends StatelessWidget {
  const Echo({
    Key key,  
    @required this.text,
    this.backgroundColor:Colors.grey,
  }):super(key:key);

  final String text;
  final Color backgroundColor;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

按照慣例,widget的構造函數參數應使用命名參數,命名參數中的必要參數要添加@required標註,這樣有利於靜態代碼分析器進行檢查。另外,在繼承widget時,第一個參數通常應該是Key,另外,如果Widget需要接收子Widget,那麼child或children參數通常應被放在參數列表的最後。
然後,我們可以通過如下方式來使用Echo widget。

Widget build(BuildContext context) {
  return Echo(text: "hello world");
}

運行後效果如下圖所示:
在這裏插入圖片描述

StatefulWidget

StatefulWidget 是一個可變狀態的widget。 使用setState方法管理StatefulWidget的狀態的改變。調用setState告訴Flutter框架,某個狀態發生了變化,Flutter會重新運行build方法,以便應用程序可以應用最新狀態。

和StatelessWidget一樣,StatefulWidget也是繼承自Widget類,並重寫了createElement()方法,不同的是返回的Element 對象並不相同;另外StatefulWidget類中添加了一個新的接口createState()。

下面是StatefulWidget的類定義,如下所示:

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實例。

StatelessWidget和StatefulWidget的區別

通過上面的講解,我們可以得出如下結論:

  • StatelessWidget是狀態不可變的widget, 初始狀態設置以後就不可再變化, 如果需要變化需要重新創建StatefulWidget,因爲StatefulWidget可以保存自己的狀態。
  • 在Flutter中通過引入State來保存狀態, 當State的狀態改變時,能重新構建本節點以及孩子的Widget樹來進行UI變化。
  • 如果需要主動改變State的狀態,需要通過setState()方法進行觸發,單純改變數據是不會引發UI改變的

Widgets的State

說到組件,就不得不提到Widgets的State。通常,一個StatefulWidget類會對應一個State類,State表示與其對應的StatefulWidget要維護的狀態,State中的保存的狀態信息有如下兩個作用:

  1. 在widget build時可以被同步讀取。
  2. 在widget生命週期中可以被改變,當State被改變時,可以手動調用其setState()方法通知Flutter framework狀態發生改變,Flutter framework在收到消息後,會重新調用其build方法重新構建widget樹,從而達到更新UI的目的。

State有兩個常用屬性:widget和context。

  • widget:它表示與該State實例關聯的widget實例,由Flutter framework動態設置。注意,這種關聯並非永久的,因爲在應用聲明週期中,UI樹上的某一個節點的widget實例在重新構建時可能會變化,但State實例只會在第一次插入到樹中時被創建,當在重新構建時,如果widget被修改了,Flutter framework會動態設置State.widget爲新的widget實例。
  • context,它是BuildContext類的一個實例,表示構建widget的上下文,它是操作widget在樹中位置的一個句柄,它包含了一些查找、遍歷當前Widget樹的一些方法。每一個widget都有一個自己的context對象。

生命週期

和原生平臺的控件一樣,State也有自己的生命週期。爲了加深讀者對State生命週期的印象,本節我們通過一個實例來演示一下State的生命週期。在接下來的示例中,我們實現一個計數器widget,點擊它可以使計數器加1,由於要保存計數器的數值狀態,所以我們應繼承StatefulWidget,代碼如下:

class CounterWidget extends StatefulWidget {
  const CounterWidget({
    Key key,
    this.initValue: 0
  });

  final int initValue;

  @override
  _CounterWidgetState createState() => new _CounterWidgetState();
}

CounterWidget接收一個initValue整型參數,它表示計數器的初始值。接下來,我們看一下_CounterWidgetState的實現:

class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;

  @override
  void initState() {
    super.initState();
    //初始化狀態  
    _counter=widget.initValue;
    print("initState");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(
          child: Text('$_counter'),
          //點擊後計數器自增
          onPressed:()=>setState(()=> ++_counter,
          ),
        ),
      ),
    );
  }

  @override
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }

  @override
  void deactivate() {
    super.deactivate();
    print("deactive");
  }

  @override
  void dispose() {
    super.dispose();
    print("dispose");
  }

  @override
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}

接下來,我們創建一個新路由,在新路由中,我們只顯示一個CounterWidget。

Widget build(BuildContext context) {
  return CounterWidget();
}

然後,運行應用並打開該路由頁面,在新路由頁打開後,屏幕中央就會出現一個數字0,並且控制檯日誌輸出如下:

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

可以看到,熱重載操作時initState 和didChangeDependencies都沒有被調用,而是調用了didUpdateWidget。
接下來,我們在widget樹中移除CounterWidget,並將路由build方法改爲:

Widget build(BuildContext context) {
  //移除計數器 
  //return CounterWidget();
  //隨便返回一個Text()
  return Text("xxx");
}

然後執行熱重載操作,日誌如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose

可以看到,在CounterWidget從widget樹中移除時,deactive和dispose會依次被調用。

通過上面的示例,我們將StatefulWidget生命週期整理如下圖:
在這裏插入圖片描述
StatefulWidget的生命週期大致可分爲三個階段:

  • 初始化:插入渲染樹,這一階段涉及的生命週期函數主要有createState、initState、didChangeDependencies和build。
  • 運行中:在渲染樹中存在,這一階段涉及的生命週期函數主要有didUpdateWidget和build。
  • 銷燬:從渲染樹中移除,此階段涉及的生命週期函數主要有deactivate和dispose。

初始化階段

createState:createState必須且僅執行一次,它用來創建state,當創建StatefulWidget時,該放方法就會被執行。

initState:在創建StatefulWidget後,initState是第一個被調用的方法,同createState一樣只被調用一次,此時widget的被添加至渲染樹,mount的值會變爲true,但並沒有渲染。我們可以在該方法內做一些初始化操作。

didChangeDependencies:當widget第一次被創建時,didChangeDependencies緊跟着initState函數之後調用,在widget刷新時,該方法不會被調用。它會在“依賴”發生變化時被Flutter Framework調用,這個依賴是指widget是否使用父widget中InheritedWidget的數據。也即是只有在widget依賴的InheritedWidget發生變化之後,didChangeDependencies纔會調用。
這種機制可以使子組件在所依賴的InheritedWidget變化時來更新自身!比如當主題、locale(語言)等發生變化時,依賴其的子widget的didChangeDependencies方法將會被調用。

build:build會在widget第一次創建時緊跟着didChangeDependencies方法之後和UI重新渲染時被調用。build只做widget的創建操作,如果在build裏做其他操作,會影響UI的渲染效果。

運行中

StatefulWidget運行中只會調用兩個函數,即didUpdateWidget和build。
didUpdateWidget:當組件的狀態改變的時候就會調用didUpdateWidget,比如調用了setState。

銷燬

deactivate:當State對象從樹中被移除時,會調用此回調函數,這標誌着 StatefulWidget將要執行銷燬操作。頁面切換時,也會調用它,因爲此時State在視圖樹中的位置發生了變化但是State不會被銷燬,而是重新插入到渲染樹中。 重寫的時候必須要調用 super.deactivate()

dispose:從渲染樹中移除時調用,State會永久的從渲染樹中移除,和initState正好相反mount值變味false。這時候就可以在dispose裏做一些取消監聽操作。

爲了方便讀者理解,我們看一下StatefulWidget的生命週期函數調用情況。

生命週期 調用次數 調用時間
createState 1 組件創建時
initState 1 組件創建時
didChangeDependencies n 組件創建或狀態發生變化
build n 組件創建或UI重新渲染
didUpdateWidget n 組件創建或UI重新渲染
deactivate n State對象將要移除時
dispose 1 state對象被銷燬

內置組件庫

Flutter SDK提供了一套豐富、強大的基礎組件,在基礎組件庫之上Flutter又提供了一套Material風格(Android默認的視覺風格)和一套Cupertino風格(iOS視覺風格)的組件庫。使用前只需要導入即可使用:

import 'package:flutter/widgets.dart';

基礎組件

Flutter SDK提供了很多功能豐富的基礎組件,常見的有如下一些:

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

Material組件

衆所周知,Material是Android應用默認的視覺風格,Cupertino則是iOS應用的默認視覺風格,爲了實現兩種不同的視覺風格,Flutter 在基礎組件庫之上Flutter又提供了一套Material風格和一套Cupertino風格的組件庫,以滿足兩種不同設計風格的開發需要。

Material應用程序以MaterialApp 組件開始, 該組件在應用程序的根部創建了一些必要的組件,比如Theme組件,它用於配置應用的主題。 是否使用MaterialApp完全是可選的,但是使用它是一個很好的做法。在之前的示例中,我們已經使用過多個Material 組件了,如:Scaffold、AppBar、FlatButton等。

要使用Material 組件,需要先引入它:

import 'package:flutter/material.dart';

Cupertino組件

Flutter也提供了一套豐富的Cupertino風格的組件,儘管目前還沒有Material 組件那麼豐富,但是它仍在不斷的完善中。目前,Flutter提供的Cupertino組件主要有 CupertinoTabBar、 CupertinoActivityIndicator、CupertinoPageScaffold、 CupertinoTabScaffold、 CupertinoTabView 等 。
關於Cupertino組件,大家可以參考官方的介紹:Cupertino (iOS風格) Widgets

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章