前言一:接下來一段時間我會陸續更新一些列Flutter文字教程
更新進度: 每週至少兩篇;
更新地點: 首發於公眾號,第二天更新於掘金、思否、開發者頭條等地方;
更多交流: 可以新增我的微信 372623326,關注我的微博:coderwhy
希望大家可以 幫忙轉發,點選在看,給我更多的創作動力。
一. StatefulWidget
在開發中,某些Widget情況下我們展示的資料並不是一層不變的:
比如Flutter預設程式中的計數器案例,點選了+號按鈕後,顯示的數字需要+1;
比如在開發中,我們會進行下拉重新整理、上拉載入更多,這時資料也會發生變化;
而StatelessWidget通常用來展示哪些資料固定不變的,如果資料會發生改變,我們使用StatefulWidget;
1.1. 認識StatefulWidget
1.1.1. StatefulWidget介紹
如果你有閱讀過預設我們建立Flutter的示例程式,那麼你會發現它建立的是一個StatefulWidget。
為什麼選擇StatefulWidget呢?
- 因為在示例程式碼中,當我們點選按鈕時,介面上顯示的資料會發生改變;
- 這時,我們需要一個
變數
來記錄當前的狀態,再把這個變數顯示到某個Text Widget上; - 並且每次
變數
發生改變時,我們對應的Text上顯示的內容也要發生改變;
但是有一個問題,我之前說過定義到Widget中的資料都是不可變的,必須定義為final,為什麼呢?
- 這次因為Flutter在設計的時候就決定了一旦Widget中展示的資料發生變化,就重新構建整個Widget;
- 下一個章節我會講解Flutter的渲染原理,Flutter通過一些機制來限定定義到Widget中的
成員變數
必須是final
的;
Flutter如何做到我們在開發中定義到Widget中的資料一定是final的呢?
我們來看一下Widget的原始碼:
@immutable
abstract class Widget extends DiagnosticableTree {
// ...省略程式碼
}
複製程式碼
這裡有一個很關鍵的東西@immutable
- 我們似乎在Dart中沒有見過這種語法,這實際上是一個
註解
,這設計到Dart的超程式設計,我們這裡不展開講; - 這裡我就說明一下這個@immutable是幹什麼的;
實際上官方有對@immutable進行說明:
- 來源: api.flutter.dev/flutter/met…
- 說明: 被@immutable註解標明的類或者子類都必須是不可變的
結論: 定義到Widget中的資料一定是不可變的,需要使用final來修飾
1.1.2. 如何儲存Widget狀態?
既然Widget是不可變,那麼StatefulWidget如何來儲存可變的狀態呢?
- StatelessWidget無所謂,因為它裡面的資料通常是直接定義玩後就不修改的。
- 但StatefulWidget需要有狀態(可以理解成變數)的改變,這如何做到呢?
Flutter將StatefulWidget設計成了兩個類:
- 也就是你建立StatefulWidget時必須建立兩個類:
- 一個類繼承自StatefulWidget,作為Widget樹的一部分;
- 一個類繼承自State,用於記錄StatefulWidget會變化的狀態,並且根據狀態的變化,構建出新的Widget;
建立一個StatefulWidget,我們通常會按照如下格式來做:
- 當Flutter在構建Widget Tree時,會獲取
State的例項
,並且它呼叫build方法去獲取StatefulWidget希望構建的Widget; - 那麼,我們就可以將需要儲存的狀態儲存在MyState中,因為它是可變的;
class MyStatefulWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// 將建立的State返回
return MyState();
}
}
class MyState extends State<MyStatefulWidget> {
@override
Widget build(BuildContext context) {
return <構建自己的Widget>;
}
}
複製程式碼
思考:為什麼Flutter要這樣設計呢?
這是因為在Flutter中,只要資料改變了Widget就需要重新構建(rebuild)
1.2. StatefulWidget案例
1.2.1. 案例效果和分析
我們通過一個案例來練習一下StatefulWidget,還是之前的計數器案例,但是我們按照自己的方式進行一些改進。
案例效果以及佈局如下:
- 在這個案例中,有很多佈局對於我們來說有些複雜,我們後面會詳細學習,建議大家根據我的程式碼一步步寫出來來熟悉Flutter開發模式;
- Column小部件:之前我們已經用過,當有垂直方向佈局時,我們就使用它;
- Row小部件:之前也用過,當時水平方向佈局時,我們就使用它;
- RaiseButton小部件:可以建立一個按鈕,並且其中有一個
onPress屬性
是傳入一個回撥函式
,當按鈕點選時被回撥;
1.2.2. 建立StatefulWidget
下面我們來看看程式碼實現:
- 因為當點選按鈕時,數字會發生變化,所以我們需要使用一個StatefulWidget,所以我們需要建立兩個類;
- MyCounterWidget繼承自StatefulWidget,裡面需要實現createState方法;
- MyCounterState繼承自State,裡面實現build方法,並且可以定義一些成員變數;
class MyCounterWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// 將建立的State返回
return MyCounterState();
}
}
class MyCounterState extends State<MyCounterWidget> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Text("當前計數:$counter", style: TextStyle(fontSize: 30),),
);
}
}
複製程式碼
1.2.3. 實現按鈕的佈局
class MyCounterState extends State<MyCounterWidget> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
color: Colors.redAccent,
child: Text("+1", style: TextStyle(fontSize: 18, color: Colors.white),),
onPressed: () {
},
),
RaisedButton(
color: Colors.orangeAccent,
child: Text("-1", style: TextStyle(fontSize: 18, color: Colors.white),),
onPressed: () {
},
)
],
),
Text("當前計數:$counter", style: TextStyle(fontSize: 30),)
],
),
);
}
}
複製程式碼
1.2.4. 按鈕點選狀態改變
我們現在要監聽狀態的改變,當狀態改變時要修改counter變數
:
- 但是,直接修改變數可以改變介面嗎?不可以。
- 這是因為Flutter並不知道我們的資料發生了改變,需要來重新構建我們介面中的Widget;
如何可以讓Flutter知道我們的狀態發生改變了,重新構建我們的Widget呢?
- 我們需要呼叫一個State中預設給我們提供的setState方法;
- 可以在其中的回撥函式中修改我們的變數;
onPressed: () {
setState(() {
counter++;
});
},
複製程式碼
這樣就可以實現想要的效果了:
1.3. StatefulWidget生命週期
1.3.1. 生命週期的理解
什麼是生命週期呢?
- 客戶端開發:iOS開發中我們需要知道UIViewController從建立到銷燬的整個過程,Android開發中我們需要知道Activity從建立到銷燬的整個過程。以便在不同的生命週期方法中完成不同的操作;
- 前端開發中:Vue、React開發中元件也都有自己的生命週期,在不同的生命週期中我們可以做不同的操作;
Flutter小部件的生命週期:
- StatelessWidget可以由父Widget直接傳入值,呼叫build方法來構建,整個過程非常簡單;
- 而StatefulWidget需要通過State來管理其資料,並且還要監控狀態的改變決定是否重新build整個Widget;
- 所以,我們主要討論StatefulWidget的生命週期,也就是它從建立到銷燬的整個過程;
1.3.2. 生命週期的簡單版
在這個版本中,我講解那些常用的方法和回撥,下一個版本中我解釋一些比較複雜的方法和回撥
那麼StatefulWidget有哪些生命週期的回撥呢?它們分別在什麼情況下執行呢?
- 在下圖中,灰色部分的內容是Flutter內部操作的,我們並不需要手動去設定它們;
- 白色部分表示我們可以去監聽到或者可以手動呼叫的方法;
我們知道StatefulWidget本身由兩個類組成的:StatefulWidget
和State
,我們分開進行分析
首先,執行StatefulWidget中相關的方法:
- 1、執行StatefulWidget的建構函式(Constructor)來建立出StatefulWidget;
- 2、執行StatefulWidget的createState方法,來建立一個維護StatefulWidget的State物件;
其次,呼叫createState建立State物件時,執行State類的相關方法:
-
1、執行State類的構造方法(Constructor)來建立State物件;
-
2、執行initState,我們通常會在這個方法中執行一些資料初始化的操作,或者也可能會傳送網路請求;
- 注意:這個方法是重寫父類的方法,必須呼叫super,因為父類中會進行一些其他操作;
- 並且如果你閱讀原始碼,你會發現這裡有一個註解(annotation):@mustCallSuper
-
3、執行didChangeDependencies方法,這個方法在兩種情況下會呼叫
-
- 情況一:呼叫initState會呼叫;
- 情況二:從其他物件中依賴一些資料發生改變時,比如前面我們提到的InheritedWidget(這個後面會講到);
-
4、Flutter執行build方法,來看一下我們當前的Widget需要渲染哪些Widget;
-
5、當前的Widget不再使用時,會呼叫dispose進行銷燬;
-
6、手動呼叫setState方法,會根據最新的狀態(資料)來重新呼叫build方法,構建對應的Widgets;
-
7、執行didUpdateWidget方法是在當父Widget觸發重建(rebuild)時,系統會呼叫didUpdateWidget方法;
我們來通過程式碼進行演示:
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("HelloWorld"),
),
body: HomeBody(),
),
);
}
}
class HomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("HomeBody build");
return MyCounterWidget();
}
}
class MyCounterWidget extends StatefulWidget {
MyCounterWidget() {
print("執行了MyCounterWidget的構造方法");
}
@override
State<StatefulWidget> createState() {
print("執行了MyCounterWidget的createState方法");
// 將建立的State返回
return MyCounterState();
}
}
class MyCounterState extends State<MyCounterWidget> {
int counter = 0;
MyCounterState() {
print("執行MyCounterState的構造方法");
}
@override
void initState() {
super.initState();
print("執行MyCounterState的init方法");
}
@override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
print("執行MyCounterState的didChangeDependencies方法");
}
@override
Widget build(BuildContext context) {
print("執行執行MyCounterState的build方法");
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
color: Colors.redAccent,
child: Text("+1", style: TextStyle(fontSize: 18, color: Colors.white),),
onPressed: () {
setState(() {
counter++;
});
},
),
RaisedButton(
color: Colors.orangeAccent,
child: Text("-1", style: TextStyle(fontSize: 18, color: Colors.white),),
onPressed: () {
setState(() {
counter--;
});
},
)
],
),
Text("當前計數:$counter", style: TextStyle(fontSize: 30),)
],
),
);
}
@override
void didUpdateWidget(MyCounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("執行MyCounterState的didUpdateWidget方法");
}
@override
void dispose() {
super.dispose();
print("執行MyCounterState的dispose方法");
}
}
複製程式碼
列印結果如下:
flutter: HomeBody build
flutter: 執行了MyCounterWidget的構造方法
flutter: 執行了MyCounterWidget的createState方法
flutter: 執行MyCounterState的構造方法
flutter: 執行MyCounterState的init方法
flutter: 執行MyCounterState的didChangeDependencies方法
flutter: 執行執行MyCounterState的build方法
// 注意:Flutter會build所有的元件兩次(查了GitHub、Stack Overflow,目前沒查到原因)
flutter: HomeBody build
flutter: 執行了MyCounterWidget的構造方法
flutter: 執行MyCounterState的didUpdateWidget方法
flutter: 執行執行MyCounterState的build方法
複製程式碼
當我們改變狀態,手動執行setState方法後會列印如下結果:
flutter: 執行執行MyCounterState的build方法
複製程式碼
1.3.3. 生命週期的複雜版(選讀)
我們來學習幾個前面生命週期圖中提到的屬性,但是沒有詳細講解的
1、mounted是State內部設定的一個屬性,事實上我們不瞭解它也可以,但是如果你想深入瞭解它,會對State的機制理解更加清晰;
- 很多資料沒有提到這個屬性,但是我這裡把它列出來,是內部設定的,不需要我們手動進行修改;
2、dirty state的含義是髒的State
- 它實際是通過一個Element的東西(我們還沒有講到Flutter繪製原理)的屬性來標記的;
- 將它標記為dirty會等待下一次的重繪檢查,強制呼叫build方法來構建我們的Widget;
- (有機會我專門寫一篇關於StatelessWidget和StatefulWidget的區別,講解一些它們開發中的選擇問題);
3、clean state的含義是乾淨的State
- 它表示當前build出來的Widget,下一次重繪檢查時不需要重新build;
二. Flutter的程式設計正規化
這個章節又講解一些理論的東西,可能並不會直接講授Flutter的知識,但是會對你以後寫任何的程式碼,都具備一些簡單的知道思想;
2.1. 程式設計正規化的理解
程式設計正規化對於初學程式設計的人來說是一個虛無縹緲的東西,但是卻是我們日常開發中都在預設遵循的一些模式和方法;
比如我們最為熟悉的 物件導向程式設計
就是一種程式設計正規化,與之對應或者結合開發的包括:程式導向程式設計、函數語言程式設計、面向協議程式設計;
另外還有兩個對應的程式設計正規化:指令式程式設計
和 宣告式程式設計
- 指令式程式設計: 指令式程式設計非常好理解,就是一步步給計算機命令,告訴它我們想做什麼事情;
- 宣告式程式設計: 宣告式程式設計通常是描述目標的性質,你應該是什麼樣的,依賴哪些狀態,並且當依賴的狀態發生改變時,我們通過某些方式通知目標作出相應;
上面的描述還是太籠統了,我們來看一些具體點的例子;
2.2. 前端的程式設計正規化
下面的程式碼沒有寫過前端的可以簡單看一下
下面的程式碼是在前端開發中我寫的兩個demo,作用都是點選按鈕後修改h2標籤的內容:
- 左邊程式碼: 指令式程式設計,一步步告訴瀏覽器我要做什麼事情;
- 右邊程式碼: 宣告式程式設計,我只是告訴h2標籤中我需要顯示title,當title發生改變的時候,通過一些機制自動來更新狀態;
2.3. Flutter的程式設計正規化
從2009年開始(資料來自維基百科),宣告式程式設計就開始流行起來,並且目前在Vue、React、包括iOS中的SwiftUI中以及Flutter目前都採用了宣告式程式設計。
現在我們來開發一個需求:顯示一個Hello World,之後又修改成了Hello Flutter
如果是傳統的指令式程式設計,我們開發Flutter的模式很可能是這樣的:(注意是想象中的虛擬碼)
- 整個過程,我們需要一步步告訴Flutter它需要做什麼;
final text = new Text();
var title = "Hello World";
text.setContent(title);
// 修改資料
title = "Hello Flutter";
text.setContent(title);
複製程式碼
如果是宣告式程式設計,我們通常會維護一套資料集:
- 這個資料集可能來自己父類、來自自身State管理、來自InheritedWidget、來自統一的狀態管理的地方;
- 總之,我們知道有這麼一個資料集,並且告訴Flutter這些資料集在哪裡使用;
var title = "Hello World";
Text(title); // 告訴Text內部顯示的是title
// 資料改變
title = "Hello Flutter";
setState(() => null); // 通知重新build Widget即可
複製程式碼
上面的程式碼過於簡單,可能不能體現出Flutter宣告式程式設計的優勢所在,但是在以後的開發中,我們都是按照這種模式在進行開始,我們一起來慢慢體會;
備註:所有內容首發於公眾號,之後除了Flutter也會更新其他技術文章,TypeScript、React、Node、uniapp、mpvue、資料結構與演算法等等,也會更新一些自己的學習心得等,歡迎大家關注