Flutter學習 - 狀態管理篇

前言

對于UI而言,最基礎的就是展示數據,刷新數據,Flutter提供了一套狀態管理機制來做這些事情

案例

通過一個案例來解釋Flutter的狀態管理機制是如何運作的,假設我們需要實現一個SegmentHeader,通過點擊不同的按鈕改變頁面的背景色。
第一步創建一個繼承自StatefulWidgetSegmentHeader類,同時創建_SegmentHeaderState

class SegmentHeader extends StatefulWidget {
  const SegmentHeader({super.key});

  @override
  State<StatefulWidget> createState() => _SegmentHeaderState();
}

class _SegmentHeaderState extends State<SegmentHeader> {
  @override
  Widget build(BuildContext context) {
    ...
  }
}

Flutter中Widget會隨著每次更新重新創建,但是State會保留,所以狀態字段都會放在State類中,目前需要保存的狀態就是用戶點擊了那個Segment

class _SegmentHeaderState extends State<SegmentHeader> {
  var selectedIndex = -1;

  @override
  Widget build(BuildContext context) {
    ...

_SegmentHeaderState中定義一個selectedIndex表示用戶選擇了哪個SegmentTab,接下來增加幾個tab,并響應點擊事件

class _SegmentHeaderState extends State<SegmentHeader> {
  var selectedIndex = -1;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(child: buildItem(0, "Red", Colors.red)),
        Expanded(child: buildItem(1, "Blue", Colors.blue)),
        Expanded(child: buildItem(2, "Cyan", Colors.cyan)),
        Expanded(child: buildItem(3, "Purple", Colors.purple)),
      ],
    );
  }

  Widget buildItem(int index, String title, Color color) {
    return GestureDetector(
      onTap: () {
        setState(() {
          selectedIndex = index;
        });
      },
      child: Container(
        margin: const EdgeInsets.all(5),
        height: 40,
        alignment: Alignment.center,
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(5),
            color: selectedIndex == index ? Colors.amber : Colors.white),
        child: Text(
          title,
          style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
        ),
      ),
    );
  }
}

在tab的點擊事件中,通過setState修改selectedIndex的值,并且通過判斷selectedIndex的值是否與當前tab的index相等來高亮選中的tab。最后再增加一個回調,告知上層選中的tab改變了

class SegmentHeader extends StatefulWidget {
  final Function(int, Color)? tabSelectedChanged;

  const SegmentHeader({super.key, this.tabSelectedChanged});

  @override
  State<StatefulWidget> createState() => _SegmentHeaderState();
}
class _SegmentHeaderState extends State<SegmentHeader> {
  var selectedIndex = -1;

...

  Widget buildItem(int index, String title, Color color) {
    return GestureDetector(
      onTap: () {
        setState(() {
          selectedIndex = index;
        });
        if (widget.tabSelectedChanged != null) {
          widget.tabSelectedChanged!.call(index, color);
        }
      },
      child: ...
    );
  }
}

將寫好的SegmentHeader整合到頁面中,并控制頁面的顏色

class LearnStatefulWidget extends StatefulWidget {
  const LearnStatefulWidget({super.key});
  @override
  State<StatefulWidget> createState() => _LearnStatefulWidgetState();
}

class _LearnStatefulWidgetState extends State<LearnStatefulWidget> {
  Color bgColor = Colors.white;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("基本State管理"),
      ),
      body: Column(
        children: [
          SegmentHeader(
            tabSelectedChanged: (idx, color) {
              setState(() {
                bgColor = color;
              });
            },
          ),
          Expanded(
              child: Container(
            color: bgColor,
          ))
        ],
      ),
    );
  }
}

通過SegmentHeader的回調控制bgColor,從而達到改變顏色的目的

Provider庫

在上面的案例中,我們使用系統的setState進行狀態管理,但是有個問題,子Widget向父Widget的數據傳遞只能依靠回調,這樣的話層級多了就很容易陷入回調地獄,為了更好的管理狀態,我們可以使用三方狀態管理庫,比如Flutter官方推薦的Provider,接下來我們通過Provider來改造上面的案例。首先添加依賴

dependencies:
  provider: ^6.0.0

接下來定義一個數據模型來存儲狀態,目前這個頁面需要存儲的就是當前選擇的索引和背景顏色

class SegmentPageModel extends ChangeNotifier {
  int selectedIndex = -1;
  Color bgColor = Colors.white;

  void setSelectedIndex(int val) {
    selectedIndex = val;
    notifyListeners();
  }

  void setBgColor(Color val) {
    bgColor = val;
    notifyListeners();
  }
}

這個模型繼承了ChangeNotifier,當數據改變時,調用notifyListeners來通知外部,接下來重寫SegmentHeader,由于使用了Provider,我們可以將SegmentHeader調整成為StatelessWidget

class SegmentHeader extends StatelessWidget {
  const SegmentHeader({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(child: buildItem(0, "Red", Colors.red)),
        Expanded(child: buildItem(1, "Blue", Colors.blue)),
        Expanded(child: buildItem(2, "Cyan", Colors.cyan)),
        Expanded(child: buildItem(3, "Purple", Colors.purple)),
      ],
    );
  }

  Widget buildItem(int index, String title, Color color) {
    return Consumer<SegmentPageModel>(builder: (context, value, child) {
      return GestureDetector(
        onTap: () {
          value.setSelectedIndex(index);
          value.setBgColor(color);
        },
        child: Container(
          margin: const EdgeInsets.all(5),
          height: 40,
          alignment: Alignment.center,
          decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(5),
              color:
                  value.selectedIndex == index ? Colors.amber : Colors.white),
          child: Text(
            title,
            style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
          ),
        ),
      );
    });
  }
}

這里一個重大改變就是需要動態改變的Widget被Consumer包裹起來,顧名思義,Consumer就是消費者的意思,當模型發送改變通知,Consumer就會重新build。在onTap回調里,我們調用setSelectedIndexsetBgColor觸發模型的數據更新。現在有了消費者,模型實例還缺少生產的地方,需要通過ChangeNotifierProvider Widget來生產模型實例

class LearnProvider extends StatelessWidget {
  const LearnProvider({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Provider管理"),
        ),
        body: ChangeNotifierProvider(
          create: (context) => SegmentPageModel(),
          child: Column(
            children: [
              const SegmentHeader(),
              Expanded(
                  child: Consumer<SegmentPageModel>(
                builder: (context, value, child) => Container(
                  color: value.bgColor,
                ),
              ))
            ],
          ),
        ));
  }
}

ChangeNotifierProvider的create中,返回模型實例即可,ChangeNotifierProviderConsumer子Widget都會接收到同一個模型實例。

總結

本篇博客介紹了基礎的狀態管理方式以及三方狀態管理庫Provider,當然還有很多其他狀態管理庫,比如redux,Bloc,可以根據自己項目的復雜程度進行選擇。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容