自訂 StatefulWidget


StatelessWidget 實例建立後,狀態就不會改變,例如,先前使用過的 Text,或者〈自訂 StatelessWidget〉中自訂的 HelloWidget,都是不可變動(immutable),也就是類別定義時,沒有提供可改變實例狀態的方法。

既然 StatelessWidget 不可變,父類當然 Widget 也是不可變,這就有了個問題,如果使用者操作後,畫面必須做出某種改變怎麼辦?最簡單的想法是,根據使用者的輸入操作資訊,重新建構 Widget,然後畫面就改變了,不過畫面組成很複雜的話,這種想法自然是會呈現上的效能問題。

因此設計畫面時,對於畫面上不會變動的元件,使用 StatelessWidget 來組建,既然不會變動,畫面其他會變動的部份改變時,StatelessWidget 不用重新 build

若是畫面上會變動的元件,才繼承 StatefulWidget,例如,來設計一個顯示時間的小程式:

import 'dart:async';
import 'package:flutter/material.dart';

void main() => runApp(Center(child: Time()));

class Time extends StatefulWidget {
  @override
  _TimeState createState() => _TimeState();
}

class _TimeState extends State<Time> {
  DateTime _dateTime = DateTime.now();

  @override
  void initState() {
    super.initState();
    Timer.periodic(new Duration(seconds: 1), (timer) {
      setState(() {
        _dateTime = DateTime.now();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text(
      '${_dateTime}'.substring(0, 19),
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
  }
}

Flutter 框架會在建立 State 實例後,呼叫 initState 進行狀態的初始,執行結果如下:

自訂 StatefulWidget

在範例程式中,Time 繼承了 StatefulWidget,與 StatelessWidget 不同的是,必須實作的不是 build 方法,而是 createState 方法,傳回 State 實例,在定義 StatefulWidget 時,總是會定義 State,如上例的 _TimeState

真正會改變狀態的是 State 實例,它會參考對應的 WidgetElement,在範例中使用 Timer,定時呼叫了 setState 方法,被指定的匿名函式會在該方法中執行(匿名函式執行過後不得傳回 Future,否則會拋出錯誤)。

setState 指定的匿名函式中,通常會撰寫改變 State 的流程,像範例中,就改變了 _dateTime,在 setState 方法執行完指定的匿名函式後,Widget 對應的元素會被標示為需要建構,框架會呼叫 build,這時重新建構 Widget,用來更新對應的 Widget

setState 簡單來說,就是通知 Flutter 狀態改變了,build 根據新狀態建立新的 Widget,也就是說,每次狀態變化了,就產生新的畫面組態,UI = f(state) 的概念,一種宣告式的風格。

從範例中可以看到,StatefulWidget 本身還是不可變的,真正會改變狀態的是 State 實例,就這個簡單範例來說,Time 似乎沒什麼太大作用,在比較複雜的範例中,繼承 StatefulWidget 的類別,可以準備 Statebuild 時可以共用的資料,畢竟若每次 build,全部的物件都要重新建構,是蠻耗費資源的動作。

例如,或許來改變一下文字的顏色:

import 'dart:async';
import 'package:flutter/material.dart';

void main() => runApp(
    Center(
      child: TimeText(
        // 指定顏色
        textColor: Colors.blue,
        backgroundColor: Colors.yellow,
      ),
    )
);

class TimeText extends StatefulWidget {
  TextStyle style;  // style 是可重用的

  TimeText({Color textColor, Color backgroundColor}) {
    style = TextStyle(
      color: textColor,
      backgroundColor: backgroundColor,
    );
  }

  @override
  _TimeTextState createState() => _TimeTextState();
}

class _TimeTextState extends State<TimeText> {
  DateTime _dateTime = DateTime.now();


  @override
  void initState() {
    super.initState();
    Timer.periodic(new Duration(seconds: 1), (timer) {
      setState(() {
        _dateTime = DateTime.now();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text(
        '${_dateTime}'.substring(0, 19),
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr,
        style: widget.style
    );
  }
}

State 中,可以透過 widget,取得關聯的 Widget 實例,範例中 widget.style 就取得了 TimeText 中的 style,執行結果如下:

自訂 StatefulWidget