本篇主要通過一些例項來深入分析Flutter中的key
簡介
通過key.dart中的註釋可以看到相關說明
- 它是 Widgets, Elements and SemanticsNodes 的識別符號
- 當新Widget的Key和Element相關聯的當前Widget的Key相等時,才會將Element關聯的Widget更新成最新的Widget
- 具有相同Parent的Elements,key必須唯一
- 它有兩個子類 LocalKey和GlobalKey
- 推薦 www.youtube.com/watch?v=kn0…
Flutter中的內部重建機制,有時候需要配合Key的使用才能觸發真正的“重建”,key通常在widget的建構函式中,當widget在widget樹中移動時,Keys儲存對應的state,在實際中,這將能幫助我們儲存使用者滑動的位置,修改widget集合等等
什麼是key
大多數時候我們並不需要key,但是當我們需要對具有某些狀態且相同型別的元件 進行 新增、移除、或者重排序時,那就需要使用key,否則就會遇到一些古怪的問題,看下例子,
點選介面上的一個按鈕,然後交換行中的兩個色塊
**
StatelessWidget 實現
使用 StatelessWidget
(StatelessColorfulTile
) 做 child
(tiles
):
class PositionedTiles extends StatefulWidget {
@override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
List<Widget> tiles = [
StatelessColorfulTile(),
StatelessColorfulTile(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
);
}
swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
class StatelessColorfulTile extends StatelessWidget {
Color myColor = UniqueColorGenerator.getColor();
@override
Widget build(BuildContext context) {
return Container(
color: myColor, child: Padding(padding: EdgeInsets.all(70.0)));
}
}
複製程式碼
StatefulWidget 實現
使用 StatefulWidget
(StatefulColorfulTile
) 做 child
(tiles
):
List<Widget> tiles = [
StatefulColorfulTile(),
StatefulColorfulTile(),
];
...
class StatefulColorfulTile extends StatefulWidget {
@override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
@override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
@override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0),
));
}
}
複製程式碼
結果點選切換顏色按鈕,沒有反應了
為了解決這個問題,我們在StatefulColorfulTile widget構造時傳入一個UniqueKey
class _ScreenState extends State<Screen> {
List<Widget> widgets = [
StatefulContainer(key: UniqueKey(),),
StatefulContainer(key: UniqueKey(),),
];
···
複製程式碼
然後點選切換按鈕,又可以愉快地交換顏色了。
為什麼StatelessWidget正常更新,StatefullWidget就更新失效,加了key之後又可以了呢?為了弄清楚這其中發生了什麼,我們需要再次弄清楚Flutter中widget的更新原理
在framework.dart中可以看到 關於widget的程式碼
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
···
/// Whether the `newWidget` can be used to update an [Element] that currently
/// has the `oldWidget` as its configuration.
///
/// An element that uses a given widget as its configuration can be updated to
/// use another widget as its configuration if, and only if, the two widgets
/// have [runtimeType] and [key] properties that are [operator==].
///
/// If the widgets have no key (their key is null), then they are considered a
/// match if they have the same type, even if their children are completely
/// different.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
複製程式碼
由於widget只是一個無法修改的配置,而Element才是真正被修改使用的物件,在前面的文章可以知道,當新的widget到來時將會呼叫canUpdate方法來確定這個Element是否需要更新,從上面可以看出,canUpdate 對兩個(新老) Widget 的 runtimeType 和 key 進行比較,從而判斷出當前的** Element 是否需要更新**。
- StatelessContainer比較
我們並沒有傳入key,所以只比較兩個runtimeType,我們將color定義在widget中,這將使得他們具有不同的runtimeType,因此能夠更新element 顯示出交換位置的效果
- StatefulContainer比較過程
改成stateful之後 我們將color的定義放在在State中,Widget並不儲存State,真正hold State的引用是Stateful Element,在我們沒有給widget設定key之前,將只會比較這兩個widget的runtimeType,由於兩個widget的屬性和方法都相同,canUpdate方法將返回false,在Flutter看來,沒有發生變化,因此點選按鈕 色塊並沒有交換,當我們給widget一個key以後,canUpdate方法將會比較兩個widget的runtimeType以及key,返回true(這裡runtimeType相同,key不同),這樣就可以正確感知兩個widget交換順序,但是這種比較也是有範圍的tu
如何正確設定key
為了提升效能,Flutter的diff演算法是有範圍的,會對某一個層級的widget進行比較而不是一個個比較,我們把上面的ok的例子再改動一下,將帶key的 StatefulContainer 包裹上 Padding 元件,然後點選交換按鈕,會發生下面奇怪的現象。點選之後不是交換widget,而是重新建立了!
@override
void initState() {
super.initState();
tiles = [
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: UniqueKey()),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulColorfulTile(key: UniqueKey()),
),
];
}
複製程式碼
對應的widget和Element樹如下
為什麼會出現這種問題,上面提到,Flutter 的
Elemetn to Widget
匹配演算法將一次只檢查樹的一個層級:,(1)顯然Padding並沒有發生本質的變化
(2)於是開始第二層的對比,此時發現元素與元件的Key並不匹配,於是把它設定成不可用狀態,但是這裡的key是本地key,(Local Key),Flutter並不能找到另一層裡面的Key(另外一個Padding Widget中的key),因此flutter就建立了一個新的
因此為了解決這個問題,我們需要將key放到Row的children這一層
class _ScreenState extends State<Screen> {
List<Widget> widgets = [
Padding(
key: UniqueKey(),
padding: const EdgeInsets.all(8.0),
child: StatefulContainer(),
),
Padding(
key: UniqueKey(),
padding: const EdgeInsets.all(8.0),
child: StatefulContainer(),
),
];
複製程式碼