在〈Widget 的 Key(二)〉談到了個既有的 Element
會被丟棄的情況,新的 Element
建立,因而也產生新的 State
物件。
Element
是透過 Widget
的 createElement
建立,而狀態是透過 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
,具有相同 key
的 Widget
就不能放到多個位置了,會發生 Each child must be laid out exactly once. 的錯誤。)
這代表著,如果你的 Widget
若是 StatefulWidget
,當 Widget
被放到 Widget
樹時,就會呼叫 createState
,在 StatefulWidget
的 createState
文件說明就寫著:
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
樹移下又放上等情況,這意謂著,如果 StatefulWidget
從 Widget
樹移下又放上,舊有的狀態就沒了。例如改一下〈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
,而現在 Note
是 StatelessWidget
。
你應該留意 createState
是否會被多次呼叫的問題,必要時將狀態儲存下來,並在 State
物件初始狀態時,嘗試載入成為畫面狀態。
或者是考慮狀態管理由哪個元件來做,子元件?父元件?或是兩者之混合?這些在〈Managing state〉中有些範例可以參考。