1. 前言
上一篇文章 Flutter渲染之通過demo瞭解Key的作用裡面通過兩個小 demo 講了 Key 的作用。其實在寫小 demo 的過程中也碰到一個問題,開始是沒有想清楚的,後來跟同事交流,去原始碼裡面翻了翻才找到一些原因,這裡再來寫一下。
2. 現象
在 demo2--交換兩個小部件 開始提到,如果兩個widget 是繼承的 StatelessWidget,結果是可成功交換的,完整程式碼如下。
class KeyDemo1Page extends StatefulWidget {
@override
_KeyDemo1PageState createState() => _KeyDemo1PageState();
}
class _KeyDemo1PageState extends State<KeyDemo1Page> {
List<Widget> tiles = [
StatelessColorfulTile(),
StatelessColorfulTile(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue,
centerTitle: true,
title: Text("Key demo"),
),
body: Column(children: tiles,),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.delete),
onPressed: (){
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
},
),
);
}
}
class StatelessColorfulTile extends StatelessWidget {
Color myColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));
@override
Widget build(BuildContext context) {
print("build hahahahahha");
return Container(
color: myColor,
child: Padding(padding: EdgeInsets.all(70.0))
);
}
}
複製程式碼
如果將 body 裡面的 Column 換為 ListView,tiles 賦值給 ListView 的 children,那麼就不行了。點選按鈕是沒有切換的。這就很奇怪了,同樣是容器啊,為啥一個可以一個不可以。為此我試了多種情況。
如果將點選按鈕交換 tiles 陣列元素改為刪除一個元素,介面也還是沒有反應; 如果將交換元素改為新建立一個陣列,將 tiles 元素插入到新陣列裡面,然後讓tiles 指向新陣列,這樣結果就是好的。
也就是說改變 tiles 陣列裡面元素的位置或者增減元素介面都不會有反應,只有改變 tiles 指向介面才可以切換。
將 ListView(children:tiles) 改為 ListView.builder 的形式是可以的,如下程式碼。
class _KeyDemo1PageState extends State<KeyDemo1Page> {
......
Widget _itemForRow(BuildContext context, int index) {
return tiles[index];
}
@override
Widget build(BuildContext context) {
return Scaffold(
......
body: ListView.builder(
itemCount: tiles.length,
itemBuilder: _itemForRow,
),
......
);
}
}
複製程式碼
3. 原因
使用 ListView 和 ListView.builder 是有區別的,因此可以對比著去原始碼裡面看看。
ListView 的構造方法如下
istView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
}) : childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? children.length,
dragStartBehavior: dragStartBehavior,
);
複製程式碼
children 也賦值到 SliverChildListDelegate 裡面去了,因此也去它裡面看看,在 SliverChildListDelegate 裡面發現了一個重要的方法,如下
@override
bool shouldRebuild(covariant SliverChildListDelegate oldDelegate) {
return children != oldDelegate.children;
}
複製程式碼
發現了,如果 ListView 的孩子陣列沒有改變的話 shouldRebuild 就會返回 false,也就不會執行 build 方法了,因此介面也不會有什麼變化。可以想象 ListView.builder 裡面也有這種方法,不過返回結果應該不一樣,去看看吧。
ListView.builder 的建構函式這樣的
ListView.builder({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
}) : assert(itemCount == null || itemCount >= 0),
assert(semanticChildCount == null || semanticChildCount <= itemCount),
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
),
super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? itemCount,
dragStartBehavior: dragStartBehavior,
);
複製程式碼
我們去 SliverChildBuilderDelegate 裡面看看,發現也有一個 shouldRebuild 方法,程式碼如下。
@override
bool shouldRebuild(covariant SliverChildBuilderDelegate oldDelegate) => true;
複製程式碼
因此可以看出使用 ListView.builder 的話 shouldRebuild 預設返回 true,因此這種方法build 都會執行,所以介面是可以切換的。
4. 總結
執行 setState 的後,ListView 的 children 裡面的元素改變,包括移動和增刪,介面是不會改變的,只能是改變整個 children 指向。使用 ListView.builder 則會改變。
從開始發現問題到嘗試了各種現象發現一些基本規律,然後去原始碼裡面找原因,基本可以搞清楚問題所在,不過還是有一些技術細節需要更深入一層理解了。