一.引入key的概念
- 這里有一個小demo
- 每次點擊按鈕沥阱,刪除第一個Widget
1.使用StatefulWidget
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
final List<Widget> _widgets = [const StateFulTest('1111'), const StateFulTest('2222'), const StateFulTest('33333')];
void _onPressed() {
setState(() {
_widgets.removeAt(0);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Key Demo'),),
floatingActionButton: FloatingActionButton(
onPressed: _onPressed,
child: const Icon(Icons.add),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _widgets,
)
);
}
}
class StateFulTest extends StatefulWidget {
const StateFulTest(this.title ,{Key? key}) : super(key: key);
final String title;
@override
_StateFulTestState createState() => _StateFulTestState();
}
class _StateFulTestState extends State<StateFulTest> {
Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: color,
child: Center(
child: Text(widget.title),
),
);
}
}
class StatelessTest extends StatelessWidget {
StatelessTest(this.title, {Key? key}) : super(key: key);
final String title;
Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: color,
child: Center(
child: Text(title),
),
);
}
}
key_colors_bug.gif
文字顯示正常膀息,但是Widget的顏色卻是不正常的
- 看起來就像刪除的第三個Widget
2.使用StatelessWidget
- 那這里可否懷疑是Stateful導致的問題芹扭?
- 將
_widgets
中的StateFulTest
更換為StatelessDemo
- 發(fā)現居然正常了
3.使用StatelessWidget芯肤,將State中的color放到Widget
- 經過測試正常
4.問題排查思路
- 出現該問題是因為state沒有被刷新或者重置
- 通過渲染原理可知state的創(chuàng)建是在StatefulElement的構造方法中问拘,Element與state是綁定
- 出現的問題是因為刪除后的第一個Widget綁定了之前刪掉的Element遍略,導致state不會被刷新,出現顏色不會變化的bug
- 針對Widget刷新時骤坐,綁定了之前一個Element導致的bug绪杏。查看Widget源碼是否有關于Widget與Element綁定的方法
-
在Widget類中,有一種非常重要的方法
canUpdate
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
- 進入這個方法的前提是2個Widget的父Element是相同的
- 這個函數的注釋寫得非常清楚纽绍,新的Wdiget是否可以更新到老的Widget的Elment
- 簡答來說蕾久,是否可以復用Element
- 還有一點我們也需要了解,新老Widget的子部件不同也不會影響到是否可以update
-
runtimeType
為對象的運行時類型的形式 - 通過這個方法我們可以很明確的分析到拌夏,很明顯新老Widget是一種類型僧著,并且runtimeType肯定是一致的。key沒有傳值為nil障簿,因此該方法必定返回true盹愚。
- 將Element1更新到Widget2中
- 至此,問題問題排查清楚
-
下面用一張圖來簡單的分析一下
statefulWidget_Element_Error.png
5.當我們移除一個Widget時站故,同時再添加一個Widget皆怕,此時的Element是否會復用移除的?
final List<Widget> _widgets = [const StateFulTest('1111'), const StateFulTest('2222'), const StateFulTest('33333')];
void _onPressed() {
setState(() {
_widgets.removeAt(0);
_widgets.add(const StateFulTest('4444'));
});
}
1.添加一個不帶key的Widget
按上圖邏輯,Element3會被釋放
那么現在斷點調試西篓,查看一下Flutter在這塊是怎么優(yōu)化的
此時斷點斷在createElement()
StatefulElement createElement() => StatefulElement(this);
結果:并沒有進入斷點愈腾,添加新的Widget(不含key)沒有執(zhí)行createElement
2.添加一個帶key的Widget
void _onPressed() {
setState(() {
_widgets.removeAt(0);
_widgets.add(const StateFulTest('4444', key: ValueKey(4),));
});
}
結果:進入斷點,添加新的Widget(含key)執(zhí)行createElement
3.總結
在setState方法中岂津,當我們移除一個Widget時虱黄,同時再添加一個Widget,此時的Element會先去復用移除的(canUpdate判斷是否能復用)
二.Key
- 抽象類寸爆,一般使用它的派生類
LocalKey
和GlobalKey
abstract class Key {
/// Construct a [ValueKey<String>] with the given [String].
///
/// This is the simplest way to create keys.
const factory Key(String value) = ValueKey<String>;
/// Default constructor, used by subclasses.
///
/// Useful so that subclasses can call us, because the [new Key] factory
/// constructor shadows the implicit constructor.
@protected
const Key.empty();
}
- 默認Key的工程構造方法也是使用ValueKey實現礁鲁,ValueKey為LocalKey的派生類
- 在之前的代碼修改_widgets盐欺,加入key
final List<Widget> _widgets = [const StateFulTest('1111', key: Key('1111'),), const StateFulTest('2222', key: Key('2222')), const StateFulTest('33333', key: Key('33333'))];
key_colors_ferfect.gif
- 結果顯示正常
- 至此關于構造方法中Key的作用相信大家應該比較明白了
三.LocalKey
- 一般用于相同父Element小部件的比較。也就是在Widget中update方法使用
1.ValueKey
- 指的是通過一個值來創(chuàng)建的key仅醇。其中傳入的值類型是泛型冗美,任意類型
- 使用場景,通過value值來對比
2.ObjectKey
- 指的是通過一個對象來創(chuàng)建的key
- 使用場景析二,通過Object指針地址來對比
3.UniqueKey
- 指的是創(chuàng)建了一個唯一的key粉洼。通過該對象生成一個唯一的hash碼
- 使用場景,每次構建時key都是不同的叶摄,因此Element永遠不會復用
keyDemo() {
//創(chuàng)建測試對象
TestKeyClass testK = TestKeyClass();
//ValueKey
ValueKey key1 = ValueKey(testK);
ValueKey key2 = ValueKey(testK);
ValueKey key3 = const ValueKey(3);
print(key1 == key2); //true
print(key1 == key3); //false
//ObjectKey
ObjectKey objectKey1 = ObjectKey(testK);
ObjectKey objectKey2 = ObjectKey(testK);
ObjectKey objectKey3 = ObjectKey(TestKeyClass());
print(objectKey1 == objectKey2); //true
print(objectKey1 == objectKey3); //false
//UniqueKey
print(UniqueKey() == UniqueKey()); //false
}
四.GlobalKey
- 一般通過使用GlobalKey來保存/獲取某一部件的Widget属韧、State、Element
- 概念類似于iOS中的tag
- 這里介紹一個簡單的使用場景蛤吓,在StatelessWidget中刷新StatefulWidget的狀態(tài)
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
final GlobalKey _globalKey = GlobalKey();
void _onPressed() {
_GlobalKeyTestState state = _globalKey.currentState as _GlobalKeyTestState ;
/*
* 下面寫法會報出警告
* The member 'setState' can only be used within instance members of subclasses of 'package:flutter/src/widgets/framework.dart'.
* 大致意思是setState這個方法應該只能在state方法里面調用
* 因此這里寫了一個refreshState方法中轉一下來消除警告
* */
// state.setState(() {
// state.count ++;
// });
state.count ++;
state.refreshState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Global Key Demo'),),
body: GlobalKeyTest(key: _globalKey,),
floatingActionButton: FloatingActionButton(
onPressed: _onPressed,
child: const Icon(Icons.add),
),
),
);
}
}
class GlobalKeyTest extends StatefulWidget {
const GlobalKeyTest({Key? key}) : super(key: key);
@override
_GlobalKeyTestState createState() => _GlobalKeyTestState();
}
class _GlobalKeyTestState extends State<GlobalKeyTest> {
var count = 0;
refreshState() {
setState(() {
});
}
@override
Widget build(BuildContext context) {
return Center(
child: Text('$count'),
);
}
}
globalKey.gif