首先,我們先附上官網教程中關於flutter框架的介紹,作爲補充。
Flutter官方提供的Flutter框架圖
Flutter Framework
這是一個純 Dart實現的 SDK:
Foundation和Animation、Painting、Gestures 在Google的一些視頻中被合併爲一個dart UI層,對應的是Flutter中的dart:ui包,它是Flutter引擎暴露的底層UI庫,提供動畫、手勢及繪製能力。
Rendering層是一個抽象的佈局層,它依賴於dart UI層,Rendering層會構建一個UI樹,當UI樹有變化時,會計算出有變化的部分,然後更新UI樹,最終將UI樹繪製到屏幕上,這個過程類似於React中的虛擬DOM。
Widgets層是Flutter提供的的一套基礎組件庫,在基礎組件庫之上,Flutter還提供了 Material 和Cupertino兩種視覺風格的組件庫。
Flutter Engine
這是一個純 C++實現的 SDK,其中包括了 Skia引擎、Dart運行時、文字排版引擎等。在代碼調用 dart:ui庫時,調用最終會走到Engine層,然後實現真正的繪製邏輯。
在我們學習flutter的過程中,很多老師大咖們最喜歡說的一句話就是flutter一切皆widget。
Flutter中Widget不僅可以表示UI元素,也可以表示一些功能性的組件如:用於手勢檢測的 GestureDetector widget、用於APP主題數據傳遞的Theme等等,而原生開發中的控件通常只是指UI元素。
我們先來看一下Widget類的聲明:
/// Describes the configuration for an [Element].
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;
}
}
其中有一句註釋我特別給大家列出來了:Describes the configuration for an [Element].
在Flutter中,Widget的功能是“描述一個UI元素的配置數據”,也就是說,Widget其實並不是表示最終繪製在設備屏幕上的顯示元素,而它只是描述顯示元素的一個配置數據。實際上在flutter中真正代表屏幕上顯示元素的類是Element。對於我們這些希望能夠達到使用目的的初學者來說,我覺得沒有必要過於深究。
我們只需要知道:Widget只是UI元素的一個配置數據,並且一個Widget可以對應多個Element。這是因爲同一個Widget對象可以被添加到UI樹的不同部分,而真正渲染時,UI樹的每一個Element節點都會對應一個Widget對象。
再來看看類接口:
widget類繼承自DiagnosticableTree,DiagnosticableTree即“診斷樹”,主要作用是提供調試信息。
Key: 這個key屬性類似於React/Vue中的key,主要的作用是決定是否在下一次build時複用舊的widget,決定的條件在canUpdate()方法中。
createElement**()**:Flutter Framework在構建UI樹時,會先調用此方法生成對應節點的Element對象。此方法是Flutter Framework隱式調用的,在我們開發過程中基本不會調用到。
debugFillProperties(…) 複寫父類的方法,主要是設置診斷樹的一些特性。
**canUpdate(…)**是一個靜態方法,可以理解爲是否用新的Widget對象去更新舊UI樹上所對應的Element對象的配置。
Widget 類本身是一個抽象類,其中最核心的就是定義了createElement()接口,在Flutter開發中,我們一般都不用直接繼承Widget類來實現一個新組件,相反,我們通常會通過繼承StatelessWidget或StatefulWidget來間接繼承Widget類來實現。
StatelessWidget和StatefulWidget都是直接繼承自Widget類,而這兩個類也正是Flutter中非常重要的兩個抽象類,它們引入了兩種Widget模型:有狀態的組件(Stateful widget) 和無狀態的組件(Stateless widget)。
無狀態的組件(Stateless widget)
StatelessWidget用於不需要維護狀態的場景,也就是說它初始化後就不會更改,比如標題等等。一般情況下,如果widget對應的ui不需要進行更新時,那麼我們就應該使用StatelessWidget。
它通常在build方法中通過嵌套其它Widget來構建UI,在構建過程中會遞歸的構建其嵌套的Widget。
StatelessWidget繼承自Widget類,重寫了createElement()方法:
@override
StatelessElement createElement() => new StatelessElement(this);
StatelessElement 間接繼承自Element類,與StatelessWidget相對應(作爲其配置數據)。
接下來我們看一個簡單的示例:
import "package:flutter/material.dart";
class QStatelessWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text("hello flutter");
}
}
在main.dart中使用:
1.導入自定義的widget
2.在需要使用的地方創建對應的實例
import "package:flutter/material.dart";
import 'basic/QStatelessWidget.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Flutter 基礎"),
),
body: Center(
child:QStatelessWidget(),
),
));
}
}
運行結果:
顯然這樣簡單的使用並不能滿足我們的胃口,接下來我們在看看怎麼使用widget的構造函數進行傳參。
按照慣例,widget的構造函數參數應使用命名參數,命名參數中的必要參數要添加@required標註,這樣有利於靜態代碼分析器進行檢查。另外,在繼承widget時,第一個參數通常應該是Key,另外,如果Widget需要接收子Widget,那麼child或children參數通常應被放在參數列表的最後。同樣是按照慣例,Widget的屬性應儘可能的被聲明爲final,防止被意外改變。
import "package:flutter/material.dart";
class QStatelessWidget extends StatelessWidget {
QStatelessWidget({Key key, @required this.text, this.fontColor: Colors.green})
: super(key: key);
final String text;
final Color fontColor;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Text(
text,
style: TextStyle(
color: fontColor,
fontSize: 24,
fontFamily: "Courier",
decoration: TextDecoration.underline,
decorationStyle: TextDecorationStyle.dashed),
),
),
);
}
}
其中構造函數:
QStatelessWidget({Key key, @required this.text, this.fontColor: Colors.green})
: super(key: key);
這裏使用的是dart的語法糖,(詳細請參考dart的相關文檔)等價於下面這種形式:
QStatelessWidget(String text, Color fontColor=Colors.green){
this.text = text;
this.fontColor = fontColor;
}
this.fontColor: Colors.green相當於其他語言中的默認參數值,這點大家想必都知道。
build方法有一個context參數,它是BuildContext類的一個實例,表示當前widget在widget樹中的上下文,每一個widget都會對應一個context對象(因爲每一個widget都是widget樹上的一個節點)。實際上,context是當前widget在widget樹中位置中執行”相關操作“的一個句柄,比如它提供了從當前widget開始向上遍歷widget樹以及按照widget類型查找父級widget的方法
具體的代碼沒有什麼看的,這裏就不在贅述了。在使用的地方我們使用命名參數傳遞對應的參數即可。
如果我們沒有使用命名參數,那麼會得到下面的錯誤提示,:
運行效果:
有狀態的組件(Stateful widget)
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();
}
接下來我們創建一個基於statefulwidget的例子。剛創建出來的文件可能向下面的例子一樣簡單,我們一步步開始。
一個StatefulWidget類會對應一個State類,State表示與其對應的StatefulWidget要維護的狀態
createState() 用於創建和Stateful widget相關的狀態,它在Stateful widget的生命週期中可能會被多次調用。例如,當一個Stateful widget同時插入到widget樹的多個位置時,Flutter framework就會調用該方法爲每一個位置生成一個獨立的State實例,其實,本質上就是一個StatefulElement對應一個State實例。
State中的保存的狀態信息的用處:
1.在widget 構建時可以被同步讀取。
2.在widget生命週期中可以被改變,當State被改變時,可以手動調用其setState()方法通知Flutter framework狀態發生改變,Flutter framework在收到消息後,會重新調用其build方法重新構建widget樹,從而達到更新UI的目的。
接下來我們來個簡單的例子:
import 'package:flutter/material.dart';
class QStatefulWidget extends StatefulWidget {
const QStatefulWidget({Key key, this.current: 0}) : super(key: key);
final int current;
@override
_QStatefulWidgetState createState() => _QStatefulWidgetState();
}
class _QStatefulWidgetState extends State<QStatefulWidget> {
int current;
final welcomeArray = const [
"Welcome,first.",
"Welcome,second.",
"Welcome,third.",
"Welcome,fourth."
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FlatButton(
child: Text(welcomeArray[current]),
onPressed: () => setState(this.handlePressed),
),
),
);
}
void handlePressed() {
if (current == welcomeArray.length - 1) {
current = 0;
} else {
++current;
}
}
@override
void initState() {
// TODO: implement initState
super.initState();
current = widget.current;
}
}
代碼十分簡單,在屏幕中央顯示一個扁平按鈕,當點擊的時候我們會切換其上面顯示的文字。
在main.dart中指定其初始顯示的文字索引
運行效果:
點擊後:
作爲我們日後使用最多的widget,我們有必要了解一下它的生命週期,先貼一張StatefulWidget生命週期圖
ok,我們將其中用到的方法重載,添加log語句,直觀的看下其各個狀態的變化。
@override
void initState() {
// TODO: implement initState
super.initState();
print("initState function run");
current = widget.current;
}
@override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
print("didChangeDependencies function run");
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
print("dispose function run");
}
@override
void reassemble() {
// TODO: implement reassemble
super.reassemble();
print("reassemble function run");
}
@override
void deactivate() {
// TODO: implement deactivate
super.deactivate();
print("deactivate function run");
}
@override
void didUpdateWidget(QStatefulWidget oldWidget) {
// TODO: implement didUpdateWidget
super.didUpdateWidget(oldWidget);
print("didUpdateWidget function run");
}
重新運行程序,打印內容如下:
點擊按鈕:
在main.dart中移除該widget
熱加載後輸出:
initState:當Widget第一次插入到Widget樹時會被調用,對於每一個State對象,Flutter framework只會調用一次該回調,所以,通常在該回調中做一些一次性的操作,如狀態初始化、訂閱子樹的事件通知等。
didChangeDependencies:當State對象的依賴發生變化時會被調用。
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則會調用此回調。
deactivate:當State對象從樹中被移除時,會調用此回調。
dispose:當State對象從樹中被永久移除時調用。
更多的內容我們暫時不深入探究,我們只要瞭解了其各個狀態即可。
另外,Flutter Widget build方法可能會執行多次,所以在build方法內做的事情應該儘可能的少。
在使用StatefulWidget的過程中將會調用到setState((){}) 的代碼儘可能的和要更新的視圖封裝在一個儘可能小的模塊裏。