Flutter(七)之有狀態的StatefulWidget

coderwhy發表於2019-09-23

前言一:接下來一段時間我會陸續更新一些列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進行說明:

image-20190917202801994

結論: 定義到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屬性是傳入一個回撥函式,當按鈕點選時被回撥;

image-20190917214653945

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),),
    );
  }
}
複製程式碼

image-20190917215514053

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),)
        ],
      ),
    );
  }
}
複製程式碼

image-20190917215915106

1.2.4. 按鈕點選狀態改變

我們現在要監聽狀態的改變,當狀態改變時要修改counter變數

  • 但是,直接修改變數可以改變介面嗎?不可以。
  • 這是因為Flutter並不知道我們的資料發生了改變,需要來重新構建我們介面中的Widget;

如何可以讓Flutter知道我們的狀態發生改變了,重新構建我們的Widget呢?

  • 我們需要呼叫一個State中預設給我們提供的setState方法;
  • 可以在其中的回撥函式中修改我們的變數;
onPressed: () {
  setState(() {
    counter++;
  });
},
複製程式碼

這樣就可以實現想要的效果了:

image-20190917220412775

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本身由兩個類組成的:StatefulWidgetState,我們分開進行分析

Flutter(七)之有狀態的StatefulWidget

首先,執行StatefulWidget中相關的方法:

  • 1、執行StatefulWidget的建構函式(Constructor)來建立出StatefulWidget;
  • 2、執行StatefulWidget的createState方法,來建立一個維護StatefulWidget的State物件;

其次,呼叫createState建立State物件時,執行State類的相關方法:

  • 1、執行State類的構造方法(Constructor)來建立State物件;

  • 2、執行initState,我們通常會在這個方法中執行一些資料初始化的操作,或者也可能會傳送網路請求;

    • 注意:這個方法是重寫父類的方法,必須呼叫super,因為父類中會進行一些其他操作;
    • 並且如果你閱讀原始碼,你會發現這裡有一個註解(annotation):@mustCallSuper

    image-20190918212956907

  • 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的機制理解更加清晰;

  • 很多資料沒有提到這個屬性,但是我這裡把它列出來,是內部設定的,不需要我們手動進行修改;

image-20190918212620587

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發生改變的時候,通過一些機制自動來更新狀態;

image-20190919120003281

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、資料結構與演算法等等,也會更新一些自己的學習心得等,歡迎大家關注

公眾號

相關文章