我們之前已經使用過很多組件了,像Container
、Row
、或者ElevatedButton
等 Widget,使用頻率還是比較多的
但不知你有沒有注意到,他們的構造函數中,都会有一個 Key,作為構造器的屬性,它通常作為第一個屬性存在。
在 flutter 中,Key 可以標識一個唯一的組件,正如其名,Key 一半用來做唯一的標識
我們之前沒有使用這個 Key,這時候就會有兩種情況:
- 組件類型不同:比如
Container
和SizedBox
,此時 Flutter 內部可以通過組件的類型區分,此時無需使用key - 組件類型相同,比如由多個
Container
組成的數組,此時 Flutter 無法通過組件類型區分多個組件,此時就需要使用Key
默認情況下,如果沒有定義 key,flutter 默認並不会自動創建 key,而是使用 widget 的類型和 index 來調用。
我們可以通過一個案例來描述沒有 key 造成的問題:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Widget> list = [];
@override
void initState() {
super.initState();
list = [
const Box(color: Colors.blue),
const Box(color: Colors.yellow),
const Box(color: Colors.red),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
list.shuffle(); //shuffle可以隨機打亂此列表的元素
});
},
child: const Icon(Icons.refresh),
),
);
}
}
class Box extends StatefulWidget {
final Color color;
const Box({Key? key, required this.color}) : super(key: key);
@override
State<Box> createState() => _BoxState();
}
class _BoxState extends State<Box> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.transparent,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color)),
onPressed: () {
setState(() {
_count++;
});
},
child: Text(
"$_count",
style: Theme.of(context).textTheme.headlineLarge,
),
),
);
}
}
以上是一個 StatefulWidget 的例子,通過點擊floatingActionButton
打亂顯示在螢幕上的三個 Box 的順序,效果如下
(圖片)
可以看到,雖然點擊重排按鈕,box 表面上改變了順序,但是裡面的 State,也就是數字的顯示,並沒有隨之改變,這是因為 Flutter 識別到這三個 Box 是一樣的,只會做 stateless 層面的改變,而 State 中的東西是無法動的,所以才會造成這種現象(說的有點不太好,之後補充)
這時候就需要使用 key 來幫助我們了:
Key 作為唯一標識#
Key 可以分為兩大類:GlobalKey
和LocalKey
,顧名思義,Global 是全局的,Local 是本地的(非全局)
//GlobalKey可以共享給全局使用
final GlobalKey _globalKey1 = GlobalKey();
final GlobalKey _globalKey2 = GlobalKey();
final GlobalKey _globalKey3 = GlobalKey();
//GlobalKey 是非常昂貴的,需要合理使用
//LocalKey
//ValueKey是LocalKey
const Box(color: Colors.blue, key: ValueKey(1)),
//ObjectKey也是LocalKey
const Box(color: Colors.red, key: ObjectKey(Text("key"))),
//UniqueKey也是LocalKey,作為獨一無二的存在(生成一個具有唯一性的 hash 碼)
Box(color: Colors.red, key: UniqueKey()),
//PageStorageKey可以當前保存頁面的狀態,例如列表的滾動狀態
PageStorageKey()
LocalKey
與GlobalKey
的區別:
LocalKey
應用於有相同父節點的比較情況,也就是一個父節點管理多個子節點,子節點最好使用LocalKey
GlobalKey
能夠跨 Widget 訪問狀態,應用於有多個父節點的比較情況,也就是子節點對應多個父節點,比如下面會說的的橫豎屏切換功能,需要在Row
和Colunm
中切換,如果只是用LocalKey
的話,會丟失State
現有的狀態。
以上是定義GlobalKey
和LocalKey
的方式。
現在讓我們給上面實例的 Box 加上 Key:
以下的案例會使用到MediaQuery
中的orientation
屬性查詢設備橫豎屏狀態
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
//GlobalKey,可以共享給全局,這樣在橫豎屏切換的時候,切換組件就不會丟失狀態了
final GlobalKey _globalKey1 = GlobalKey();
final GlobalKey _globalKey2 = GlobalKey();
final GlobalKey _globalKey3 = GlobalKey();
List<Widget> list = [];
@override
void initState() {
super.initState();
list = [
Box(color: Colors.blue, key: _globalKey1),
Box(color: Colors.yellow, key: _globalKey2),
Box(color: Colors.red, key: _globalKey3),
];
}
//我們使用localKey(ValueKey)會丟失狀態
// List<Widget> list = [
// const Box(color: Colors.blue, key: ValueKey(1)),
// const Box(color: Colors.yellow, key: ValueKey(2)),
// const Box(color: Colors.red, key: ValueKey(3)),
// ];
@override
Widget build(BuildContext context) {
//豎屏protrait橫屏landscape
print(MediaQuery.of(context).orientation);
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
//橫屏橫向顯示,豎屏豎向顯示
//但是這樣更改以後,狀態就無法保存了,因為Column和Row的key還有組件本身就是不一樣的
child: MediaQuery.of(context).orientation == Orientation.portrait
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
list.shuffle(); //shuffle可以隨機打亂此列表的元素
});
},
child: const Icon(Icons.refresh),
),
);
}
}
效果如下:
注意:使用
GlobalKey
開銷較大,如果有其他可選方案,應盡量避免使用它。另外,同一個GlobalKey
在整個 widget 樹中必須是唯一的,不能重複。
使用 GlobalKey 操作子元素#
GlobalKey 有一個非常重要的特性:可以跨 Widget 訪問 State
舉個例子,如果想獲取子 Widget(Box)內的 State,我們可以這樣做
var boxState = _globalKey.currentState as _BoxState;
拿到了boxState
之後,就可以直接調用BoxState
中的方法、屬性了
另外,我們還可以獲取子 widget 節點
var boxWidget = _globalKey.currentWidget as Box;
這樣可以拿到 box 中的各種屬性了,比如背景顏色,寬高,字體大小等
完整演示:
//父widget
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
//GlobalKey,可以共享給全局
final GlobalKey _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: Center(
child: Box(color: Colors.blue, key: _globalKey),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
//父組件獲取子元素的statewidget裡的屬性*
var boxState = _globalKey.currentState as _BoxState;
setState(() {
boxState._count++;
});
print(boxState._count);
//調用子Widget的方法
boxState.run();
//獲取子widget
var boxWidget = _globalKey.currentWidget as Box;
print(boxWidget.color); //MaterialColor(primary value: Color(0xff2196f3))
//獲取子組件渲染的屬性
var renderBox =
_globalKey.currentContext!.findRenderObject() as RenderBox;
print(renderBox.size); // Size(100.0, 100.0)
},
child: const Icon(Icons.add),
),
);
}
}
//子widget
class Box extends StatefulWidget {
final Color color;
const Box({Key? key, required this.color}) : super(key: key);
@override
State<Box> createState() => _BoxState();
}
class _BoxState extends State<Box> {
int _count = 0;
void run() {
print("我是box的Run方法");
}
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.transparent,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color)),
onPressed: () {
setState(() {
_count++;
});
},
child: Text(
"$_count",
style: Theme.of(context).textTheme.headlineLarge,
),
),
);
}
}