在開發 Flutter 的過程當中你可能會發現,一些小部件的構造函數中都有一個可選的參數——Key。剛接觸的同窗或許會對這個概念感到很迷茫,感到不知所措。算法
在這篇文章中咱們會深刻淺出的介紹什麼是 Key,以及應該使用 key 的具體場景。less
在 Flutter 中咱們常常與狀態打交道。咱們知道 Widget 能夠有 Stateful 和 Stateless 兩種。Key 可以幫助開發者在 Widget tree 中保存狀態,在通常的狀況下,咱們並不須要使用 Key。那麼,究竟何時應該使用 Key呢。dom
咱們來看看下面這個例子。ide
class StatelessContainer extends StatelessWidget {
final Color color = RandomColor().randomColor();
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: color,
);
}
}
複製代碼
這是一個很簡單的 Stateless Widget,顯示在界面上的就是一個 100 * 100 的有顏色的 Container。 RandomColor 可以爲這個 Widget 初始化一個隨機顏色。函數
咱們如今將這個Widget展現到界面上。佈局
class Screen extends StatefulWidget {
@override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
List<Widget> widgets = [
StatelessContainer(),
StatelessContainer(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widgets,
),
),
floatingActionButton: FloatingActionButton(
onPressed: switchWidget,
child: Icon(Icons.undo),
),
);
}
switchWidget(){
widgets.insert(0, widgets.removeAt(1));
setState(() {});
}
}
複製代碼
這裏在屏幕中心展現了兩個 StatelessContainer 小部件,當咱們點擊 floatingActionButton 時,將會執行 switchWidget 並交換它們的順序。post
看上去並無什麼問題,交換操做被正確執行了。如今咱們作一點小小的改動,將這個 StatelessContainer 升級爲 StatefulContainer。class StatefulContainer extends StatefulWidget {
StatefulContainer({Key key}) : super(key: key);
@override
_StatefulContainerState createState() => _StatefulContainerState();
}
class _StatefulContainerState extends State<StatefulContainer> {
final Color color = RandomColor().randomColor();
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: color,
);
}
}
複製代碼
在 StatefulContainer 中,咱們將定義 Color 和 build 方法都放進了 State 中。性能
如今咱們仍是使用剛纔同樣的佈局,只不過把 StatelessContainer 替換成 StatefulContainer,看看會發生什麼。ui
這時,不管咱們怎樣點擊,都再也沒有辦法交換這兩個Container的順序了,而 switchWidget 確實是被執行了的。this
爲了解決這個問題,咱們在兩個 Widget 構造的時候給它傳入一個 UniqueKey。
class _ScreenState extends State<Screen> {
List<Widget> widgets = [
StatefulContainer(key: UniqueKey(),),
StatefulContainer(key: UniqueKey(),),
];
···
複製代碼
而後這兩個 Widget 又能夠正常被交換順序了。
看到這裏你們確定心中會有疑問,爲何 Stateful Widget 沒法正常交換順序,加上了 Key 以後就能夠了,在這之中到底發生了什麼? 爲了弄明白這個問題,咱們將涉及 Widget 的 diff 更新機制。
在以前的文章中,咱們介紹了 Widget 和 Element 的關係。若你還對 Element 的概念感到很模糊的話,請先閱讀 Flutter | 深刻理解BuildContext。
下面來來看Widget的源碼。
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
···
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 是否須要更新。若 canUpdate 方法返回 true 說明不須要替換 Element,直接更新 Widget 就能夠了。
在 StatelessContainer 中,咱們並無傳入 key ,因此只比較它們的 runtimeType。這裏 runtimeType 一致,canUpdate 方法返回 true,兩個 Widget 被交換了位置,StatelessElement 調用新持有 Widget 的 build 方法從新構建,在屏幕上兩個 Widget 便被正確的交換了順序。
而在 StatefulContainer 的例子中,咱們將 color 的定義放在了 State 中,Widget 並不保存 State,真正 hold State 的引用的是 Stateful Element。
當咱們沒有給 Widget 任何 key 的時候,將會只比較這兩個 Widget 的 runtimeType 。因爲兩個 Widget 的屬性和方法都相同,canUpdate 方法將會返回 true,因而更新 StatefulWidget 的位置,這兩個 Element 將不會交換位置。可是原有 Element 只會從它持有的 state 實例中 build 新的 widget。由於 element 沒變,它持有的 state 也沒變。因此顏色不會交換。這裏變換 StatefulWidget 的位置是沒有做用的。
而咱們給 Widget 一個 key 以後,canUpdate 方法將會比較兩個 Widget 的 runtimeType 以及 key。並返回 false。(這裏 runtimeType 相同,key 不一樣)
此時 RenderObjectElement 會用新 Widget 的 key 在老 Element 列表裏面查找,找到匹配的則會更新 Element 的位置並更新對應 renderObject 的位置,對於這個例子來說就是交換了 Element 的位置並交換了對應 renderObject 的位置。都交換了,那麼顏色天然也就交換了。
這裏感謝ad6623對以前錯誤描述的指出。
爲了提高性能 Flutter 的比較算法(diff)是有範圍的,它並非對第一個 StatefulWidget 進行比較,而是對某一個層級的 Widget 進行比較。
···
class _ScreenState extends State<Screen> {
List<Widget> widgets = [
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulContainer(key: UniqueKey(),),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: StatefulContainer(key: UniqueKey(),),
),
];
···
複製代碼
在這個例子中,咱們將兩個帶 key 的 StatefulContainer 包裹上 Padding 組件,而後點擊交換按鈕,會發生下面這件奇妙的事情。
兩個 Widget 的 Element 並非交換順序,而是被從新建立了。
在 Flutter 的比較過程當中它下到 Row 這個層級,發現它是一個 MultiChildRenderObjectWidget(多子部件的 Widget)。而後它會對全部 children 層逐個進行掃描。
在Column這一層級,padding 部分的 runtimeType 並無改變,且不存在 Key。而後再比較下一個層級。因爲內部的 StatefulContainer 存在 key,且如今的層級在 padding 內部,該層級沒有多子 Widget。runtimeType 返回 flase,Flutter 的將會認爲這個 Element 須要被替換。而後從新生成一個新的 Element 對象裝載到 Element 樹上替換掉以前的 Element。第二個 Widget 同理。
因此爲了解決這個問題,咱們須要將 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(),
),
];
···
複製代碼
如今咱們又能夠愉快的玩耍了(交換 Widget 順序)了。
slot 可以描述子級在其父級列表中的位置。多子部件 Widget 例如 Row,Column 都爲它的子級提供了一系列 slot。
在調用 Element.updateChild 的時候有一個細節,若新老 Widget 的實例相同,注意這裏是實例相同而不是類型相同, slot 不一樣的時候,Flutter 所作的僅僅是更新 slot,也就給他換個位置。因 爲 Widget 是不可變的,實例相贊成味着顯示的配置相同,因此要作的僅僅是挪個地方而已。
abstract class Element extends DiagnosticableTree implements BuildContext {
···
dynamic get slot => _slot;
dynamic _slot;
···
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
···
if (child != null) {
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
assert(child.widget == newWidget);
assert(() {
child.owner._debugElementWasRebuilt(child);
return true;
}());
return child;
}
deactivateChild(child);
assert(child._parent == null);
}
return inflateWidget(newWidget, newSlot);
}
複製代碼
更新機制表
新WIDGET爲空 | 新 Widget不爲空 | |
---|---|---|
child爲空 | 返回null。 | 返回新的 Element |
child不爲空 | 移除舊的widget,返回null. | 若舊的child Element 能夠更新(canUpdate)則更新並將其返回,不然返回一個新的 Element. |
@immutable
abstract class Key {
const factory Key(String value) = ValueKey<String>;
@protected
const Key.empty();
}
複製代碼
默認建立 Key 將會經過工廠方法根據傳入的 value 建立一個 ValueKey。
Key 派生出兩種不一樣用途的 Key:LocalKey 和 GlobalKey。
LocalKey 直接繼承至 Key,它應用於擁有相同父 Element 的小部件進行比較的狀況,也就是上述例子中,有一個多子 Widget 中須要對它的子 widget 進行移動處理,這時候你應該使用Localkey。
Localkey 派生出了許多子類 key:
Valuekey 又派生出了 PageStorageKey : PageStorageKey('value')
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
···
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
static final Set<Element> _debugIllFatedElements = HashSet<Element>();
static final Map<GlobalKey, Element> _debugReservations = <GlobalKey, Element>{};
···
BuildContext get currentContext ···
Widget get currentWidget ···
T get currentState ···
複製代碼
GlobalKey 使用了一個靜態常量 Map 來保存它對應的 Element。
你能夠經過 GlobalKey 找到持有該GlobalKey的 Widget,State 和 Element。
注意:GlobalKey 是很是昂貴的,須要謹慎使用。
若是您有一個 Todo List 應用程序,它將會記錄你須要完成的事情。咱們假設每一個 Todo 事情都各不相同,而你想要對每一個 Todo 進行滑動刪除操做。
這時候就須要使用 ValueKey!
return TodoItem(
key: ValueKey(todo.task),
todo: todo,
onDismissed: (direction){
_removeTodo(context, todo);
},
);
複製代碼
若是你有一個生日應用,它能夠記錄某我的的生日,並用列表顯示出來,一樣的仍是須要有一個滑動刪除操做。
咱們知道人名可能會重複,這時候你沒法保證給 Key 的值每次都會不一樣。可是,當人名和生日組合起來的 Object 將具備惟一性。
這時候你須要使用 ObjectKey!
若是組合的 Object 都沒法知足惟一性的時候,你想要確保每個 Key 都具備惟一性。那麼,你可使用 UniqueKey。它將會經過該對象生成一個具備惟一性的 hash 碼。
不過這樣作,每次 Widget 被構建時都會去從新生成一個新的 UniqueKey,失去了一致性。也就是說你的小部件仍是會改變。(還不如不用😂)
當你有一個滑動列表,你經過某一個 Item 跳轉到了一個新的頁面,當你返回以前的列表頁面時,你發現滑動的距離回到了頂部。這時候,給 Sliver 一個 PageStorageKey!它將可以保持 Sliver 的滾動狀態。
GlobalKey 可以跨 Widget 訪問狀態。 在這裏咱們有一個 Switcher 小部件,它能夠經過 changeState 改變它的狀態。
class SwitcherScreenState extends State<SwitcherScreen> {
bool isActive = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Switch.adaptive(
value: isActive,
onChanged: (bool currentStatus) {
isActive = currentStatus;
setState(() {});
}),
),
);
}
changeState() {
isActive = !isActive;
setState(() {});
}
}
複製代碼
可是咱們想要在外部改變該狀態,這時候就須要使用 GlobalKey。
class _ScreenState extends State<Screen> {
final GlobalKey<SwitcherScreenState> key = GlobalKey<SwitcherScreenState>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: SwitcherScreen(
key: key,
),
floatingActionButton: FloatingActionButton(onPressed: () {
key.currentState.changeState();
}),
);
}
}
複製代碼
這裏咱們經過定義了一個 GlobalKey 並傳遞給 SwitcherScreen。而後咱們即可以經過這個 key 拿到它所綁定的 SwitcherState 並在外部調用 changeState 改變狀態了。
這篇文章的靈感來自於 什麼時候使用密鑰 - Flutter小部件 101 第四集, 強烈建議你們觀看這個系列視頻,你會對 Flutter 如何構建視圖更加清晰。也但願這篇文章對你有所幫助!
在這個視頻最後介紹 GlobalKey 時,提到了 Globalkey 可以用於在不一樣小部件之間同步狀態,以及保存狀態的功能,但我並無找到實現辦法,若是有使用過這兩個功能的小夥伴麻煩在這篇文章下面留言告訴我一下,謝謝!😙
文章如有不對之處還請各位高手指出,歡迎在下方評論區以及個人郵箱1652219550a@gmail.com留言,我會在24小時內與您聯繫!