Flutter(七)之有狀態的StatefulWidget

前言一:接下來一段時間我會陸續更新一些列Flutter文字教程

更新進度: 每週至少兩篇;html

更新地點: 首發於公衆號,次日更新於掘金、思否、開發者頭條等地方;前端

更多交流: 能夠添加個人微信 372623326,關注個人微博:coderwhyvue

但願你們能夠 幫忙轉發,點擊在看,給我更多的創做動力。算法

一. StatefulWidget

在開發中,某些Widget狀況下咱們展現的數據並非一層不變的:

好比Flutter默認程序中的計數器案例,點擊了+號按鈕後,顯示的數字須要+1;編程

好比在開發中,咱們會進行下拉刷新、上拉加載更多,這時數據也會發生變化;api

而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,咱們分開進行分析

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

  • 一、執行StatefulWidget的構造函數(Constructor)來建立出StatefulWidget;
  • 二、執行StatefulWidget的createState方法,來建立一個維護StatefulWidget的State對象;

其次,調用createState建立State對象時,執行State類的相關方法:

  • 一、執行State類的構造方法(Constructor)來建立State對象;
  • 二、執行initState,咱們一般會在這個方法中執行一些數據初始化的操做,或者也可能會發送網絡請求;

    • 注意:這個方法是重寫父類的方法,必須調用super,由於父類中會進行一些其餘操做;
    • 而且若是你閱讀源碼,你會發現這裏有一個註解(annotation):@mustCallSuper

image-20190918212956907

  • 三、執行didChangeDependencies方法,這個方法在兩種狀況下會調用
    • 狀況一:調用initState會調用;
    • 狀況二:從其餘對象中依賴一些數據發生改變時,好比前面咱們提到的InheritedWidget(這個後面會講到);
  • 四、Flutter執行build方法,來看一下咱們當前的Widget須要渲染哪些Widget;
  • 五、當前的Widget再也不使用時,會調用dispose進行銷燬;
  • 六、手動調用setState方法,會根據最新的狀態(數據)來從新調用build方法,構建對應的Widgets;
  • 七、執行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. 生命週期的複雜版(選讀)

咱們來學習幾個前面生命週期圖中提到的屬性,可是沒有詳細講解的

一、mounted是State內部設置的一個屬性,事實上咱們不瞭解它也能夠,可是若是你想深刻了解它,會對State的機制理解更加清晰;

  • 不少資料沒有提到這個屬性,可是我這裏把它列出來,是內部設置的,不須要咱們手動進行修改;

image-20190918212620587

二、dirty state的含義是髒的State

  • 它實際是經過一個Element的東西(咱們尚未講到Flutter繪製原理)的屬性來標記的;
  • 將它標記爲dirty會等待下一次的重繪檢查,強制調用build方法來構建咱們的Widget;
  • (有機會我專門寫一篇關於StatelessWidget和StatefulWidget的區別,講解一些它們開發中的選擇問題);

三、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、數據結構與算法等等,也會更新一些本身的學習心得等,歡迎你們關注

公衆號

相關文章
相關標籤/搜索