Introduction
Một trong những widget cơ bản mà các developer sử dụng thường xuyên đó là ListView.
ListView rất dễ dùng, nhưng để tối ưu nó thì không hẳn ai cũng biết. Trong bài viết này, mình sẽ cùng đi vào một usecase rất phổ biến đó là lồng listview trong listview và bài toán performance.
Ví dụ bên dưới đây cho bạn thấy cách mà ta lồng các ListView con vào 1 ListView thông thường.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ListView(
// Setting `shrinkWrap` to `true` here is both unnecessary and expensive.
children: <Widget>[
ListView.builder(
itemCount: list1Children.length,
itemBuilder: (BuildContext context, int index) {
return list1Children[index];
},
// This forces the `ListView` to build all of its children up front,
// negating much of the benefit of using `ListView.builder`.
shrinkWrap: true,
),
ListView.builder(
itemCount: list2Children.length,
itemBuilder: (BuildContext context, int index) {
return list2Children[index];
},
// This forces the `ListView` to build all of its children up front,
// negating much of the benefit of using `ListView.builder`.
shrinkWrap: true,
),
...
],
)
Hãy cùng xem lại về định nghĩa của shrinkWrap
ở đây
Việc set
shrinkWrap = true
sẽ force toàn bộ listview bên trong của bạn tính toán lại sao cho chiều cao của list là hữu hạn thay vì chiều cao thông thường của các listview là vô cùng. Điều này sẽ gây giảm hiệu năng hơn đáng kể bởi kích thước của scrollview luôn cần được tính toán lại bất cứ khi nào vị trí scroll thay đổi.
Giờ thì bạn đã hiểu tại sao phần code bên trên sẽ khá tốn kém
rồi phải không :D
Có 2 vấn đề bạn cần phải để ý :
ListView
bao ngoài cùng đang không setshrinkWrap = true
, mà chỉ listview con cần set mà thôiListView.Builder
hiệu quả trong việc build các thành phần con của nó, tránh việc phải build những widget không thuộc viewport. Nhưng việc setshrinkWrap = true
sẽ overrides lại cách làm này, khi đó ListView.Builder sẽ không còn ý nghĩa nữa.
Everything builds!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() {
runApp(ShrinkWrApp());
}
class ShrinkWrApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ShrinkWrap vs Slivers',
home: Scaffold(
appBar: AppBar(
title: const Text("ShrinkWrap, Street Rat, I don't, Buy that!"),
),
body: const ShrinkWrapSlivers(),
),
);
}
}
class ShrinkWrapSlivers extends StatefulWidget {
const ShrinkWrapSlivers({
Key? key,
}) : super(key: key);
@override
_ShrinkWrapSliversState createState() => _ShrinkWrapSliversState();
}
class _ShrinkWrapSliversState extends State<ShrinkWrapSlivers> {
List<ListView> innerLists = [];
final numLists = 15;
final numberOfItemsPerList = 100;
@override
void initState() {
super.initState();
for (int i = 0; i < numLists; i++) {
final _innerList = <ColorRow>[];
for (int j = 0; j < numberOfItemsPerList; j++) {
_innerList.add(const ColorRow());
}
innerLists.add(
ListView.builder(
itemCount: numberOfItemsPerList,
itemBuilder: (BuildContext context, int index) => _innerList[index],
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
),
);
}
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: numLists,
itemBuilder: (context, index) => innerLists[index]);
}
}
@immutable
class ColorRow extends StatefulWidget {
const ColorRow({Key? key}) : super(key: key);
@override
State createState() => ColorRowState();
}
class ColorRowState extends State<ColorRow> {
Color? color;
@override
void initState() {
super.initState();
color = randomColor();
}
@override
Widget build(BuildContext context) {
print('Building ColorRowState');
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
randomColor(),
randomColor(),
],
),
),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(height: 50, width: 50, color: Colors.white),
),
Flexible(
child: Column(
children: const <Widget>[
Padding(
padding: EdgeInsets.all(8),
child: Text('I\'m a widget!',
style: TextStyle(color: Colors.white)),
),
],
),
),
],
),
);
}
}
Color randomColor() =>
Color((math.Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
Nếu chạy thử, bạn sẽ thấy 1 vấn đề xảy ra, đó là ngay khi UI được load, 100 item đầu được render ngay mặc dù ta mới chỉ nhìn thấy vài item đầu… Và khi bạn scroll thêm một đoạn đủ 100 item, thì 100 item tiếp theo sẽ được load ngay sau đó tại 1 thời điểm. Đây như là một vụ bom nổ vậy :D
Giờ phải làm như nào ???
Lazily building nested lists
Để cứu bạn, hãy cùng xay dựng lại giao diện với Slivers
thay vì ListViews.
Để tìm hiểu thêm về sliver, hãy đọc ở đây
Bước 1
Thay ListView wrap ngoài cùng bằng SliverList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Before
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: numberOfLists,
itemBuilder: (context, index) => innerLists[index],
);
}
// After
@override
Widget build(BuildContext context) {
return CustomScrollView(slivers: innerLists);
}
Bước 2
Thay đổi data type của listview con từ List
1
2
3
4
5
6
// Before
List<ListView> innerLists = [];
// After
List<SliverList> innerLists = [];
Bước 3
Giờ thì restructure đống listview con nào. SliverList thì hơi khác so với ListView, chủ yếu từ sự xuất hiện của 1 thằng là delegate.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Before
@override
void initState() {
super.initState();
for (int i = 0; i < numberOfLists; i++) {
final _innerList = <ColorRow>[];
for (int j = 0; j < numberOfItemsPerList; j++) {
_innerList.add(const ColorRow());
}
innerLists.add(
ListView.builder(
itemCount: numberOfItemsPerList,
itemBuilder: (BuildContext context, int index) => _innerList[index],
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
),
);
}
}
// After
@override
void initState() {
super.initState();
for (int i = 0; i < numLists; i++) {
final _innerList = <ColorRow>[];
for (int j = 0; j < numberOfItemsPerList; j++) {
_innerList.add(const ColorRow());
}
innerLists.add(
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => _innerList[index],
childCount: numberOfItemsPerList,
),
),
);
}
}
Và đây là thành quả
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() {
runApp(SliversApp());
}
class SliversApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'ShrinkWrap vs Slivers',
home: Scaffold(
appBar: AppBar(
title: const Text("Revenge of the Slivers"),
),
body: const ShrinkWrapSlivers(),
),
);
}
}
class ShrinkWrapSlivers extends StatefulWidget {
const ShrinkWrapSlivers({
Key? key,
}) : super(key: key);
@override
_ShrinkWrapSliversState createState() => _ShrinkWrapSliversState();
}
class _ShrinkWrapSliversState extends State<ShrinkWrapSlivers> {
List<SliverList> innerLists = [];
final numLists = 15;
final numberOfItemsPerList = 100;
@override
void initState() {
super.initState();
for (int i = 0; i < numLists; i++) {
final _innerList = <ColorRow>[];
for (int j = 0; j < numberOfItemsPerList; j++) {
_innerList.add(const ColorRow());
}
innerLists.add(
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => _innerList[index],
childCount: numberOfItemsPerList,
),
),
);
}
}
@override
Widget build(BuildContext context) {
return CustomScrollView(slivers: innerLists);
}
}
@immutable
class ColorRow extends StatefulWidget {
const ColorRow({Key? key}) : super(key: key);
@override
State createState() => ColorRowState();
}
class ColorRowState extends State<ColorRow> {
Color? color;
@override
void initState() {
super.initState();
color = randomColor();
}
@override
Widget build(BuildContext context) {
print('Building ColorRowState');
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
randomColor(),
randomColor(),
],
),
),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(height: 50, width: 50, color: Colors.white),
),
Flexible(
child: Column(
children: const <Widget>[
Padding(
padding: EdgeInsets.all(8),
child: Text('I\'m a widget!',
style: TextStyle(color: Colors.white)),
),
],
),
),
],
),
);
}
}
Color randomColor() =>
Color((math.Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
Hãy chạy thử đoạn code trên, bạn sẽ thấy rằng UI sẽ không còn render 100 item 1 lúc mà chỉ render 1 phần rất nhỏ. Còn gì tuyệt hơn khi scroll đến cuối danh sách mà không mất bất kì chi phí tốn kém nào.
Conclusion
Cảm ơn các bạn đã đọc bài viết. Happy coding ^^
References
- https://www.youtube.com/watch?v=LUqDNnv_dh0