iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 4
0
Mobile Development

Why Flutter why? 從表層到底層,從如何到為何。系列 第 4

days[3] = "為什麼需要狀態管理?"

狀態管理是Flutter長久以來最熱門的話題之一,事實上這在Flutter社群已經火熱/渾沌到一個荒謬的境界了,幾乎每幾週就會有新的狀態管理套件出現...

然後就會被加入這個列表...

因為這是個隕石坑,在來個信仰之躍之前,讓我們先冷靜下來回頭想想,到底什麼是狀態?為什麼我們需要管理它?

狀態是什麼?

每個人應該或多或少都對狀態有一些概念,雖然沒辦法馬上說出個明確的定義,但很輕易就能舉出一些例子:現在登入的使用者、購物車的商品、列表勾選的選項...。聽起來好像就是一些...變數?的確以最廣義的定義來說,程式運行時某一個瞬間,記憶體中所有變數的值,就可以說是這個程式當前的狀態。如果你把它全部儲存起來,下次開啟程式時又全部讀取進來,程式就會回到當初的狀態,一模一樣的UI,一模一樣的行為。這就是一個快照(snapshot)的概念。但仔細想想,我們其實不須要儲存記憶體裡的所有變數。因為有些變數,根據我們的設計,可能是不會在程式執行的時候被改變的,例如按鈕的顏色和大小、載入的字串資源和字型,或是一些程式中設定的常數。在狀態管理的情境下,這些我們自然不會把它稱作狀態,因為它不須要被管理。當然,如果我們程式的設計是會去改變按鈕的顏色,那它就是一個狀態了。另外,框架本身在執行時也存在著許多變數,隨著程式執行也會不斷改變,但框架自然會去幫你管理它。總之,我們關心的是,在程式執行時,會被外在環境(使用者、API、GPSs...)改變,並且會影響程式運作(UI和行為)的那些狀態變數。所以,我們要怎麼管理它們?不管理會怎樣?說真的,所謂的管理到底又是什麼?

在有狀態管理之前...

因為Flutter的陳述式設計,導致框架本身就強制你做基本的狀態管理,我們其實比較難看到如果沒有狀態管理會發生什麼事,但還是讓我們試著想像看看。首先以我們經典的Counter為例:

class CounterState extends State<Counter> {
  int _counter = 0;
  final textKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Count: $_counter', key: textKey),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() { _counter++; });
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

然後假設今天我們有某種神奇的方法可以直接改變widget的屬性,而且畫面也會跟著更新,那麼在不使用int _counter = 0;這個state的狀況下,我們可以這樣做:

        onPressed: () {
          String currentText = textKey.currentWidget.getText(); // magical getText function
          int currentCount = int.parse(currentText.replaceFirst('Count: ', ''));
          textKey.currentWidget.setText('Count: $currentCount'); // magical setText function
        },

首先我們可以少宣告一個變數,然後我們直接從Text上拿到現在的值,修改之後再放回Text上,聽起來滿簡單的?是不是比什麼setState好懂多了?UI上的值就是我的state!但即使是這個簡單的例子,我們也可以馬上看出它的問題:透過字串插值的語法我們可以很輕易的把count塞進字串中,但是當我們要把它取出來的時候就麻煩多了,如果我們的字串格式再稍微複雜一點,通常還會牽涉到Regex。也就是說,從狀態產生UI,或者用比較潮的寫法UI = f(state),它並不一定是個可逆函式,有時候只是比較困難,有時候則是完全不可行。
接著如果我們的UI稍微複雜一點點:

class CounterState extends State<Counter> {
  int _counter = 0;
  final textKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Text('Count: ${_counter+3}', key: textKey),
          Text('Count: ${_counter-1}', key: textKey),
          Text('Count: ${_counter*2}', key: textKey),
          Text('Count: ${_counter/5}', key: textKey),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // how do you update texts???
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

試著想想看onPressed要怎麼寫?只是加了幾個Text而已,我們的onPressed就得要一個一個去把Text上的值解析出來,加一之後再一個一個套回去。而且這還是全部都是Text,只是規則略有不同的狀況。而且這還只有一個button。如果UI是這樣:

class CounterState extends State<Counter> {
  int max = 10;
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(children: [
        Row(children: [
          Text('Current: $count'),
          if (count > max)
            Icon(Icons.warning)
        ]),
        Row(children: [
          IconButton(icon: Icon(Icons.add), onPressed: () {}),
          IconButton(icon: Icon(Icons.remove), onPressed: () {}),
        ]),
        ListView.builder(
          itemBuilder: (_, i) => Text('item $i'),
          itemCount: count,
          shrinkWrap: true,
        ),
      ]),
    );
  }
}

再來挑戰一下onPressed要怎麼寫吧?記得我們現在不用setState,而是用fooWidget.getData, barWidget.setData的方式。我們須要更新三個地方:顯示當前數量的Text,超出最大值顯示的警告Icon,還有ListView的數量。對了還有這次我們有addremove兩個button!如何?是不是想回到setState的懷抱了呢?這時候我們還是只要setState((){ count++; })就好了,太神啦!
到這裡我們應該可以更清楚的感受到狀態管理的意義了,整理一下所謂管理做的幾件事:

  1. 將會改變的狀態從UI抽離出來(count而非"Count: count")
  2. 存成容易存取/修改/觀察的變數或資料結構
  3. 狀態改變時發出通知

為什麼要做?狀態管理?就講到這裡。你可能會想,怎麼完全沒提到Provider, BLoC, MobX...?褲子都脫了就給我看setState?的確setState只是個非常基本的官方狀態管理工具,也有很多很難做或做不到的事情,而其它所有套件可以說都是為了解決setState的不足(或其它套件的衍生問題)而誕生的。不過就像一開始說的,那可是個隕石坑。而對於一些剛加入Flutter的新朋友來說,在被一大堆狀態管理套件搞得暈頭轉向之前,能夠更清楚的瞭解,我們最初到底想靠狀態管理解決什麼問題,從中獲得什麼優點,也是相當重要的。至於那些大名鼎鼎的套件,未來有機會再聊吧。


上一篇
days[2] = "為什麼選擇Dart?"
下一篇
days[4] = "三顆渲染樹是如何運作的?"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30

尚未有邦友留言

立即登入留言