createElement 與 createState


在〈Widget 的 Key(二)〉談到了個既有的 Element 會被丟棄的情況,新的 Element 建立,因而也產生新的 State 物件。

Element 是透過 WidgetcreateElement 建立,而狀態是透過 createState 建立,在 Widget 的 API 文件上談到:

A given widget can be included in the tree zero or more times. In particular a given widget can be placed in the tree multiple times. Each time a widget is placed in the tree, it is inflated into an Element, which means a widget that is incorporated into the tree multiple times will be inflated multiple times.

也就是說,Widget 是可以重用的(畢竟它是組態物件),可以被放到 Widget 樹的多個位置,也可以從 Widget 樹取下後放到其他位置,每次 Widget 被放到 Widget 樹上,就會生成一個 Element,如果一個 Widget 被用在 Widget 樹多個位置,就會生成多個 Element

(雖說一個 Widget 可以被放置到 Widget 樹的多個位置,不過若 Widget 設置了 key,具有相同 keyWidget 就不能放到多個位置了,會發生 Each child must be laid out exactly once. 的錯誤。)

這代表著,如果你的 Widget 若是 StatefulWidget,當 Widget 被放到 Widget 樹時,就會呼叫 createState,在 StatefulWidgetcreateState 文件說明就寫著:

The framework can call this method multiple times over the lifetime of a StatefulWidget. For example, if the widget is inserted into the tree in multiple locations, the framework will create a separate State object for each location. Similarly, if the widget is removed from the tree and later inserted into the tree again, the framework will call createState again to create a fresh State object, simplifying the lifecycle of State objects.

也就是說,在 StatefulWidget 的生命週期中,createState 有可能被多次呼叫,包括 StatefulWidget 被用於多個位置,或者從 Widget 樹移下又放上等情況,這意謂著,如果 StatefulWidgetWidget 樹移下又放上,舊有的狀態就沒了。例如改一下〈Widget 的 Key(二)〉的範例:

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

void main() => runApp(MyNote());

class MyNote extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Note',
      home: MyHomePage(title: 'My Note Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({this.title});

  final String title;

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

// Note 現在繼承了 StatefulWidget
class Note extends StatefulWidget {
  double width;
  double height;

  // 設置 key
  Note({Key key, this.width, this.height}) : super(key: key);

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

class _NoteState extends State<Note> {
  Note note;
  Color color;

  _NoteState(this.note);

  @override
  void initState() {
    super.initState();

    // 隨機顏色
    var rand = new Random();
    color = Color.fromARGB(255,
        rand.nextInt(255), rand.nextInt(255), rand.nextInt(255));
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      width: note.width,
      height: note.height,
    );
  }
}

class _MyHomePageState extends State<MyHomePage> {
  final notes = [
    Note(
      key: UniqueKey(),
      width: 200,
      height: 200,
    ),
    Note(
      key: UniqueKey(),
      width: 200,
      height: 200,
    ),
  ];

  var idx = 0;
  void _round_robin() {
    setState(() {
      idx = (idx + 1) % notes.length;  // 調整索引
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: notes[idx],  // 更換 Widget
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _round_robin,
        tooltip: 'round robin',
        child: Icon(Icons.swap_vert),
      ),
    );
  }
}

以上的範例是希望透過更換 Widget,來更換最上層的便利貼顯示,雖然 Note 設置了 key,然而執行時試著按下按鈕,顏色會是隨機不斷變化,就我們的範例來說,因為有 Note 會被從 Widget 樹移除,另一個 Note 被放至 Widget 樹,這時就會再呼叫一次 createElement 方法,從而呼叫 createState,舊有的狀態就沒了。

你也許會想說,那在 Note 中保留對既有 _NoteState 的參考,令 createState 每次都傳回同一 _NoteState 實例如何呢?不能這麼做,正如 createState 名稱說的,也如 API 說的,你得傳回新建的 State 實例,若 createState 傳回舊的 State,Flutter 會噴錯誤給你。

如果 StatefulWidget 會脫離 Widget 樹,而後重新放上,那麼代表這個 StatefulWidget 的狀態會是新的,在這個情況下,若真的要令新的 State 可以接續先前狀態,就要想辦法令其在初始化時載入先前狀態,也就表示你要想辦法在其他管道儲存狀態。

如果你不想理會狀態接續問題,也可以換個設計,像是〈Widget 的 Key(二)〉中使用 Stack 來切換顯示在堆疊頂端的元件,或者是將狀態儲存在父元件,例如:

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

void main() => runApp(MyNote());

class MyNote extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Note',
      home: MyHomePage(title: 'My Note Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({this.title});

  final String title;

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

// Note 繼承 StatelessWidget
class Note extends StatelessWidget {
  Color color;
  double width;
  double height;

  Note({this.color, this.width, this.height});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      width: width,
      height: height,
    );
  }
}

class _MyHomePageState extends State<MyHomePage> {
  List<Color> colors;

  @override
  void initState() {
    super.initState();
    // 隨機顏色
    var rand = new Random();
    colors = [
      Color.fromARGB(255,
          rand.nextInt(255), rand.nextInt(255), rand.nextInt(255)),
      Color.fromARGB(255,
          rand.nextInt(255), rand.nextInt(255), rand.nextInt(255))
    ];
  }

  var idx = 0;
  void _round_robin() {
    setState(() {
      idx = (idx + 1) % colors.length;  // 調整索引
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Note(
          color: colors[idx],
          width: 200,
          height: 200,
        ),  // 更換 Widget
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _round_robin,
        tooltip: 'round robin',
        child: Icon(Icons.swap_vert),
      ),
    );
  }
}

在以上的範例中,真正的狀態是顏色,它們儲存於 _MyHomePageState,而現在 NoteStatelessWidget

你應該留意 createState 是否會被多次呼叫的問題,必要時將狀態儲存下來,並在 State 物件初始狀態時,嘗試載入成為畫面狀態。

或者是考慮狀態管理由哪個元件來做,子元件?父元件?或是兩者之混合?這些在〈Managing state〉中有些範例可以參考。