Flutter之旅:認識Widget(原始碼級)

張風捷特烈發表於2019-07-07

1.Widget的第一印象

1.1:初次的見面

首先我們來到第一次看到Widget類的場景,那時還對這個世界一無所知,
進入程式的入口時runApp函式中需要傳入一個Widget物件,這便是第一眼。
初始專案中的做法是自定義了一個MyApp類繼承自StatelessWidget。

void main()=>runApp(MyApp());

---->[flutter/lib/src/widgets/binding.dart:778]----
void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}

class MyApp extends StatelessWidget {
    //略...
}
複製程式碼

1.2:Widget在原始碼中的位置

位置:flutterSDK/packages/flutter/lib/src/widgets/framework.dart:369
首先,它在framework包中,可以說至關重要。其次它繼承自DiagnosticableTree
下圖可見Widget類在Flutter的框架層中是比較頂尖的類。

Flutter之旅:認識Widget(原始碼級)

Flutter之旅:認識Widget(原始碼級)

你之後就會知道,Widget是Flutter介面的中心,可顯示在頁面上的一切,都和Widget相關。


1.3:Widget類的構成

首先,Widget是一個抽象類,擁有一個createElement()的抽象方法返回一個Element物件。
其次,Widget類本身只有一個欄位、一個構造方法、一個抽象方法、一個靜態方法和兩個普通方法。

Flutter之旅:認識Widget(原始碼級)

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的原始碼,這裡暫時不做解讀,隨著瞭解的深入,再去看原始碼,效果會好很,現在火候還未到,有個大概印象就行了。


2.Widget的狀態

2.1:Widget的狀態概述

在Widget原始碼中明顯指出了關於Widget狀態的問題:

/// Widgets themselves have no mutable state (all their fields must be final).
/// If you wish to associate mutable state with a widget, consider using a
/// [StatefulWidget], which creates a [State] object (via
/// [StatefulWidget.createState]) whenever it is inflated into an element and
/// incorporated into the tree.
    Widget的本身沒有可變狀態(所有欄位都必須是final)。
    如果您希望將一個widget擁有可變狀態,請考慮使用 StatefulWidget,
    每當它被載入為元素併合併到渲染樹中時,會建立State物件(通過 StatefulWidget.createState)。
複製程式碼

對StatefulWidget和StatelessWidget也做了簡要的描述

///  * [StatefulWidget] and [State], for widgets that can build differently
///    several times over their lifetime.
///  * [StatelessWidget], for widgets that always build the same way given a
///    particular configuration and ambient state.
    StatefulWidget和State,用於可以在其生命週期內多次構建的widget。
    StatelessWidget,用於在給定配置和環境的狀態的下始終以相同方式構建的widget。
複製程式碼

2.2: StatelessWidget 無狀態元件

該類的本身非常簡潔,由於Widget有一個createElement抽象方法,
StatelessWidget類中通過StatelessElement物件完成了該抽象方法,
所以StatelessWidget只需要關注build這個抽象方法即可。

Flutter之旅:認識Widget(原始碼級)

abstract class StatelessWidget extends Widget {
  const StatelessWidget({ Key key }) : super(key: key);
  
  @override
  StatelessElement createElement() => StatelessElement(this);

  @protected
  Widget build(BuildContext context);
複製程式碼

如初始專案中,MyApp是繼承了StatelessWidget,它的任務在於重寫build方法。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
複製程式碼

2.3:StatefulWidget有狀態元件

該類本身也比較簡單,繼承自Widget,createElement方法通過StatefulElement實現
所以該類需要注意的只有抽象方法createState(),負責返回一個State狀態物件

Flutter之旅:認識Widget(原始碼級)

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

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

  @protected
  State createState();
}
複製程式碼

初始程式碼也是用心良苦,為我們準備了一個簡單的有狀態元件MyHomePage
可以看到,該類的核心是createState方法,返回一個自定義的_MyHomePageState物件

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}
複製程式碼

2.4:State 物件

比較引人注目的就是State物件中有一個泛型,從原始碼中來看,
該泛型值接受StatefulWidget,即有狀態元件類。
State作為一個抽象類,存在一個build抽象方法來返回一個Widget物件

abstract class State<T extends StatefulWidget> extends Diagnosticable {
   //略...
   @protected
  Widget build(BuildContext context);
}
複製程式碼

初始程式碼中也為我們做了示例:
這裡將_counter作為可變狀態,通過點按鈕改變狀態,再通過setState重新渲染,
執行build方法,從而達到了點選按鈕數字增加的一個元件,

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
     //略...
    );
  }
}
複製程式碼

這裡需要注意的一點是State類中的widget屬性到底是什麼,這裡通過debug可以看出,就是傳入的泛型類,
至於如何widget屬性何時賦值以及渲染的,先別急,還有一段很長的路要走。

Flutter之旅:認識Widget(原始碼級)

現在回頭看一下,你是否已經對曾經陌生無比的初始專案有了突然熟悉了許多。
驀然回首,希望這會成為你在一段旅行中美麗的一瞬,也是我這個導遊的成功。


3.從Icon原始碼看StatelessWidget元件

趁人打鐵,為了讓大家對Widget有更好的理解,這裡挑選了兩個Widget。
通過原始碼賞析一下:一個Widget是如何構成的。第一個是無狀態家族的Icon元件

3.1:Icon元件的使用

Icon主要有三個屬性,分別控制圖示,顏色,大小

Flutter之旅:認識Widget(原始碼級)

Icon(
  Icons.local_shipping,
  color: Colors.pink,
  size: 30.0,
)
複製程式碼

3.2:Icon原始碼

從原始碼中可以看出,Icon類中主要做了四件事:
建構函式--> 宣告屬性欄位--> 實現build方法,返回Widget物件-->debugFillProperties

class Icon extends StatelessWidget {
  const Icon(
    this.icon, {
    Key key,
    this.size,
    this.color,
    this.semanticLabel,
    this.textDirection,
  }) : super(key: key);

  final IconData icon;
  final double size;
  final Color color;
  final String semanticLabel;
  final TextDirection textDirection;

  @override
  Widget build(BuildContext context) {
    //暫略...
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    //暫略...
  }
}
複製程式碼

可以看出,建構函式中有一個必須的引數icon,從定義中來看是一個IconData物件
注意:建構函式用const關鍵字修飾,欄位全被修飾為final,這就意味著欄位不可再修改。
這也是被稱為無狀態的原因,一旦物件構建完成,它就樣子就無法再被改變。

Flutter之旅:認識Widget(原始碼級)


3.3:build方法

build方法作為StatelessWidget的抽象方法,子類必須去實現
這個方法也將決定一個Widget在介面上的樣子,所以它至關重要
從原始碼中可以看出Icon主要是通過RichText來實現的,核心是text屬性

@override
Widget build(BuildContext context) {
    //略...
  Widget iconWidget = RichText(
    overflow: TextOverflow.visible, // Never clip.
    textDirection: textDirection, // Since we already fetched it for the assert...
    text: TextSpan(
      text: String.fromCharCode(icon.codePoint),//文字
      style: TextStyle(
        inherit: false,
        color: iconColor,
        fontSize: iconSize,
        fontFamily: icon.fontFamily,
        package: icon.fontPackage,
      ),
    ),
  );
  if (icon.matchTextDirection) {//做文字方向的處理
    switch (textDirection) {
      case TextDirection.rtl://文字方向從右到左
        iconWidget = Transform(
          transform: Matrix4.identity()..scale(-1.0, 1.0, 1.0),
          alignment: Alignment.center,
          transformHitTests: false,
          child: iconWidget,
        );
        break;
      case TextDirection.ltr://文字方向從左到右
        break;
    }
  }
    // 暫略...
}
複製程式碼

3.4:文字圖示的實現

映入眼簾的是String.fromCharCode()方法,它接受一個int值
這個int值是由IconData物件的codePoint屬性提供的,為了方便開發,
Flutter框架給了很多Icon靜態常量,當然你也可以使用自定義的圖示。

---->[flutter/bin/cache/pkg/sky_engine/lib/core/string.dart:134]----
external factory String.fromCharCode(int charCode);

----[flutter/lib/src/widgets/icon_data.dart:22]----
  const IconData(
    this.codePoint, {
    this.fontFamily,
    this.fontPackage,
    this.matchTextDirection = false,
  });

  /// The Unicode code point at which this icon is stored in the icon font.
  final int codePoint;
  
----[flutter/lib/src/widgets/icon_data.dart:22]----
static const IconData local_shipping = IconData(0xe558, fontFamily: 'MaterialIcons');
複製程式碼

一個圖示實際上就是一個字型,可以根據一個int值和字型檔名匹配到它。
所以圖示才支援變色和改變大小等方便的功能。


3.5:關於Semantics類

還有一點不知你是否注意,最後返回的是一個包裹了iconWidget的Semantics物件
字面上來看,它是語義化的意思,那他有什麼用處呢?

return Semantics(
  label: semanticLabel,
  child: ExcludeSemantics(
    child: SizedBox(
      width: iconSize,
      height: iconSize,
      child: Center(
        child: iconWidget,
      ),
    ),
  ),
);
複製程式碼

Flutter中的MaterialApp有一個showSemanticsDebugger的屬性可以用來檢視語義化介面

Flutter之旅:認識Widget(原始碼級)

---->[main.dart:3]----
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {

    var icon = Icon(
      Icons.local_shipping,
      color: Colors.pink,
      size: 40.0,
      semanticLabel: "一個貨車圖示",
    );
    
    var scaffold=Scaffold(
      appBar: AppBar(
        title: Text('Flutter Demo Home Page'),
      ),
      body:icon, 
    );

    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      showSemanticsDebugger: true,
      home: scaffold,
    );
  }
}
複製程式碼

好了,這樣就是對於一個簡單的無狀態元件構成的簡要介紹。


4.從Checkbox看StatefulWidget元件

4.1:CheckBox的使用

有狀態元件很好理解,首先它有一個允許改變的狀態量,不如Checkbox就是選中與否
下面的測試程式碼實現了,點選切換Checkbox選中或未選中的狀態

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    var scaffold=Scaffold(
      appBar: AppBar(
        title: Text('Flutter Demo Home Page'),
      ),
      body:CheckBoxWidget(), 
    );

    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: scaffold,
    );
  }
}

class CheckBoxWidget extends StatefulWidget {
  @override
  _CheckBoxWidgetState createState() => _CheckBoxWidgetState();
}

class _CheckBoxWidgetState extends State<CheckBoxWidget> {
  bool _checked=true;//維護CheckBox框狀態
  @override
  Widget build(BuildContext context) {
    return
        Checkbox(
          value: _checked,
          activeColor: Colors.blue, //選中時的顏色
          onChanged:(value){
            setState(() {
              _checked=value;
            });
          } ,
    );
  }
}
複製程式碼

4.2:CheckBox原始碼簡析

下面是Checkbox的程式碼,繼承自StatefulWidget,所以需要實現createState方法
這時,原始碼中使用自定義的_CheckboxState類來管理狀態。

class Checkbox extends StatefulWidget {
  const Checkbox({
    Key key,
    @required this.value,
    this.tristate = false,
    @required this.onChanged,
    this.activeColor,
    this.checkColor,
    this.materialTapTargetSize,
  }) : assert(tristate != null),
       assert(tristate || value != null),
       super(key: key);

  final bool value;//是否選中
  final ValueChanged<bool> onChanged;//點選回撥
  final Color activeColor;//啟用態框顏色
  final Color checkColor;//啟用態對勾顏色
  final bool tristate;//三態
  final MaterialTapTargetSize materialTapTargetSize;
  static const double width = 18.0;

  @override
  _CheckboxState createState() => _CheckboxState();
}
複製程式碼

通過這兩個元件原始碼,可以總結出一些風格特點:

1.建構函式用const修飾,每行寫一個屬性  
2.必須的屬性用@required註解  
3.非空的屬性用assert斷言
4.欄位全是final型別
複製程式碼

_CheckboxState中的build方法返回_CheckboxRenderObjectWidget物件
CheckBox具體繪製邏輯及狀態改變,在_RenderCheckbox中實現

---->[flutter/packages/flutter/lib/src/material/checkbox.dart:140]----
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
        //略...
    }
    return _CheckboxRenderObjectWidget(
      //略...
    );
  }
}

---->[flutter/packages/flutter/lib/src/material/checkbox.dart:168]----
class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
    //略...
  @override
  _RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox(
    //略...
  );

  @override
  void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
    //略...
  }
複製程式碼

4.3:Checkbox核心繪製方法

_RenderCheckbox繼承自RenderToggleable,可以重寫paint方法
這邊簡單看一下主要的邊框和對勾的繪製方法

 // 可以看出畫筆的顏色是checkColor,以線條的形式
 void _initStrokePaint(Paint paint) {
   paint
     ..color = checkColor
     ..style = PaintingStyle.stroke
     ..strokeWidth = _kStrokeWidth;
 }
 
//繪製邊線
void _drawBorder(Canvas canvas, RRect outer, double t, Paint paint) {
  assert(t >= 0.0 && t <= 0.5);
  final double size = outer.width;
  // 當t從0.0到1.0時,逐漸填充外部矩形。
  final RRect inner = outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t));
  canvas.drawDRRect(outer, inner, paint);
}

//繪製對勾
void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) {
  assert(t >= 0.0 && t <= 1.0);
  final Path path = Path();
  const Offset start = Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45);//起始偏移點
  const Offset mid = Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7);//中間偏移點
  const Offset end = Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25);//終止偏移點
  if (t < 0.5) {//t<0.5時,繪製短邊
    final double strokeT = t * 2.0;
    final Offset drawMid = Offset.lerp(start, mid, strokeT);
    path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
    path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy);
  } else {//t>0.5時,繪製長邊
    final double strokeT = (t - 0.5) * 2.0;
    final Offset drawEnd = Offset.lerp(mid, end, strokeT);
    path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
    path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy);
    path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy);
  }
  canvas.drawPath(path, paint);
}
複製程式碼

這樣你一個StatefulWidget元件從定義到狀態,到繪製的流程應該有所瞭解


通過本篇,希望你可以對Widget有一些更深刻的理解,然而這只是開始。之後的還會對Widget做更深入的探索 本文到此接近尾聲了,如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品;如果想細細探究它,那就跟隨我的腳步,完成一次Flutter之旅。
另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,本人微訊號:zdl1994328,期待與你的交流與切磋。

相關文章