Flutter之?ListView組件使用示例詳解
ListView的默認(rèn)構(gòu)造函數(shù)定義
ListView是最常用的可滾動(dòng)組件之一,它可以沿一個(gè)方向線性排布所有子組件,并且它也支持列表項(xiàng)懶加載(在需要時(shí)才會(huì)創(chuàng)建)。我們看看ListView的默認(rèn)構(gòu)造函數(shù)定義:
ListView({
...
//可滾動(dòng)widget公共參數(shù)
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
EdgeInsetsGeometry? padding,
//ListView各個(gè)構(gòu)造函數(shù)的共同參數(shù)
double? itemExtent,
Widget? prototypeItem, //列表項(xiàng)原型,后面解釋
bool shrinkWrap = false,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double? cacheExtent, // 預(yù)渲染區(qū)域長(zhǎng)度
//子widget列表
List<Widget> children = const <Widget>[],
})
上面參數(shù)分為兩組:第一組是可滾動(dòng)組件的公共參數(shù);第二組是ListView各個(gè)構(gòu)造函數(shù)ListView有多個(gè)構(gòu)造函數(shù)的共同參數(shù),我們重點(diǎn)來看看這些參數(shù),:
- itemExtent:該參數(shù)如果不為null,則會(huì)強(qiáng)制children的“長(zhǎng)度”為itemExtent的值;這里的“長(zhǎng)度”是指滾動(dòng)方向上子組件的長(zhǎng)度,也就是說如果滾動(dòng)方向是垂直方向,則itemExtent代表子組件的高度;如果滾動(dòng)方向?yàn)樗椒较?,則itemExtent就代表子組件的寬度。在ListView中,指定itemExtent比讓子組件自己決定自身長(zhǎng)度會(huì)有更好的性能,這是因?yàn)橹付╥temExtent后,滾動(dòng)系統(tǒng)可以提前知道列表的長(zhǎng)度,而無需每次構(gòu)建子組件時(shí)都去再計(jì)算一下,尤其是在滾動(dòng)位置頻繁變化時(shí)(滾動(dòng)系統(tǒng)需要頻繁去計(jì)算列表高度)。
- prototypeItem:如果我們知道列表中的所有列表項(xiàng)長(zhǎng)度都相同但不知道具體是多少,這時(shí)我們可以指定一個(gè)列表項(xiàng),該列表項(xiàng)被稱為 prototypeItem(列表項(xiàng)原型)。指定 prototypeItem 后,可滾動(dòng)組件會(huì)在 layout 時(shí)計(jì)算一次它延主軸方向的長(zhǎng)度,這樣也就預(yù)先知道了所有列表項(xiàng)的延主軸方向的長(zhǎng)度,所以和指定 itemExtent 一樣,指定 prototypeItem 會(huì)有更好的性能。注意,itemExtent 和prototypeItem 互斥,不能同時(shí)指定它們。
shrinkWrap:該屬性表示是否根據(jù)子組件的總長(zhǎng)度來設(shè)置ListView的長(zhǎng)度,默認(rèn)值為false。默認(rèn)情況下,ListView的會(huì)在滾動(dòng)方向盡可能多的占用空間。當(dāng)ListView在一個(gè)無邊界(滾動(dòng)方向上)的容器中時(shí),shrinkWrap必須為true。- addRepaintBoundaries:該屬性表示是否將列表項(xiàng)(子組件)包裹在RepaintBoundary組件中。RepaintBoundary 讀者可以先簡(jiǎn)單理解為它是一個(gè)”繪制邊界“,將列表項(xiàng)包裹在RepaintBoundary中可以避免列表項(xiàng)不必要的重繪,但是當(dāng)列表項(xiàng)重繪的開銷非常?。ㄈ缫粋€(gè)顏色塊,或者一個(gè)較短的文本)時(shí),不添加RepaintBoundary反而會(huì)更高效。如果列表項(xiàng)自身來維護(hù)是否需要添加繪制邊界組件,則此參數(shù)應(yīng)該指定為 false。
注意:上面這些參數(shù)并非ListView特有,其它可滾動(dòng)組件也可能會(huì)擁有這些參數(shù),它們的含義是相同的。
默認(rèn)構(gòu)造函數(shù)
默認(rèn)構(gòu)造函數(shù)有一個(gè)children參數(shù),它接受一個(gè)Widget列表(List)。這種方式適合只有少量的子組件數(shù)量已知且比較少的情況,反之則應(yīng)該使用ListView.builder 按需動(dòng)態(tài)構(gòu)建列表項(xiàng)。
注意,雖然這種方式將所有children一次性傳遞給 ListView,但子組件)仍然是在需要時(shí)才會(huì)加載(build(如有)、布局、繪制),也就是說通過默認(rèn)構(gòu)造函數(shù)構(gòu)建的 ListView 也是基于 Sliver 的列表懶加載模型。
下面是一個(gè)例子:
可以看到,雖然使用默認(rèn)構(gòu)造函數(shù)創(chuàng)建的列表也是懶加載的,但我們還是需要提前將 Widget 創(chuàng)建好,等到真正需要加載的時(shí)候才會(huì)對(duì) Widget 進(jìn)行布局和繪制。
shrinkWrap: true 效果,ListView根據(jù)子視圖計(jì)算高度:

shrinkWrap: false的效果,ListView的會(huì)在滾動(dòng)方向盡可能多的占用空間。

ListView.builder
ListView.builder適合列表項(xiàng)比較多或者列表項(xiàng)不確定的情況,下面看一下ListView.builder的核心參數(shù)列表
ListView.builder({
// ListView公共參數(shù)已省略
...
required IndexedWidgetBuilder itemBuilder,
int itemCount,
...
})
itemBuilder:它是列表項(xiàng)的構(gòu)建器,類型為IndexedWidgetBuilder,返回值為一個(gè)widget。當(dāng)列表滾動(dòng)到具體的index位置時(shí),會(huì)調(diào)用該構(gòu)建器構(gòu)建列表項(xiàng)。
itemCount:列表項(xiàng)的數(shù)量,如果為null,則為無限列表。
下面看一個(gè)例子:
return ListView.builder(
itemCount: 100,
itemExtent: 50,//強(qiáng)制高度為50.0
itemBuilder: (BuildContext context,int index){
return ListTile(
leading: const Icon(Icons.person),
title: Text('$index'),
);
});
運(yùn)行效果“

ListView.separated
ListView.separated可以在生成的列表項(xiàng)之間添加一個(gè)分割組件,它比ListView.builder多了一個(gè)separatorBuilder參數(shù),該參數(shù)是一個(gè)分割組件生成器。
下面我們看一個(gè)例子:奇數(shù)行添加一條藍(lán)色下劃線,偶數(shù)行添加一條綠色下劃線。
//下劃線widget預(yù)定義以供復(fù)用。
Widget divider1=Divider(color: Colors.blue,);
Widget divider2=Divider(color: Colors.green);
return ListView.separated(
//列表項(xiàng)構(gòu)造器
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
},
//分割器構(gòu)造器
separatorBuilder: (BuildContext context, int index) {
return index%2==0?divider1:divider2;
},
itemCount: 100);
}
運(yùn)行效果:

固定高度列表
前面說過,給列表指定 itemExtent 或 prototypeItem 會(huì)有更高的性能,所以當(dāng)我們知道列表項(xiàng)的高度都相同時(shí),強(qiáng)烈建議指定 itemExtent 或 prototypeItem 。
下面看一個(gè)示例:
ListView.builder(
prototypeItem: const ListTile(
title: Text('1'),
),
itemBuilder: (BuildContext context, int index) {
return Center(child: Text('$index'),);
});
因?yàn)榱斜眄?xiàng)都是一個(gè) ListTile,高度相同,但是我們不知道 ListTile 的高度是多少,所以指定了prototypeItem ,每個(gè)item高度根據(jù)prototypeItem來定。
ListView 原理
ListView 內(nèi)部組合了 Scrollable、Viewport 和 Sliver,需要注意:
- ListView 中的列表項(xiàng)組件都是 RenderBox,并不是 Sliver, 這個(gè)一定要注意。
- 一個(gè) ListView 中只有一個(gè)Sliver,對(duì)列表項(xiàng)進(jìn)行按需加載的邏輯是 Sliver 中實(shí)現(xiàn)的。
- ListView 的 Sliver 默認(rèn)是 SliverList,如果指定了 itemExtent ,則會(huì)使用 SliverFixedExtentList;如果 prototypeItem 屬性不為空,則會(huì)使用 SliverPrototypeExtentList,無論是是哪個(gè),都實(shí)現(xiàn)了子組件的按需加載模型。
實(shí)例:無限加載列表
假設(shè)我們要從數(shù)據(jù)源異步分批拉取一些數(shù)據(jù),然后用ListView展示,當(dāng)我們滑動(dòng)到列表末尾時(shí),判斷是否需要再去拉取數(shù)據(jù),如果是,則去拉取,拉取過程中在表尾顯示一個(gè)loading,拉取成功后將數(shù)據(jù)插入列表;如果不需要再去拉取,則在表尾提示"沒有更多"。
代碼如下:
class MyListViewPage extends StatefulWidget {
const MyListViewPage({Key? key}) : super(key: key);
@override
_MyListViewPageState createState() => _MyListViewPageState();
}
class _MyListViewPageState extends State<MyListViewPage> {
static const loadingTag = "##loading##"; //表尾標(biāo)記
final _words = <String>[loadingTag];
@override
void initState() {
// TODO: implement initState
super.initState();
_retrieveData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: getAppBar('ListView'),
body: Container(
color: Colors.black.withOpacity(0.2),
child: _buildInfinite(),
),
);
}
_buildDefault() {
return ListView(
shrinkWrap: false,
padding: const EdgeInsets.all(20.0),
children: const <Widget>[
Text('I\'m dedicating every day to you'),
Text('Domestic life was never quite my style'),
Text('When you smile, you knock me out, I fall apart'),
Text('And I thought I was so smart'),
],
);
}
_buildBuilder() {
return ListView.builder(
itemCount: 100,
itemExtent: 50, //強(qiáng)制高度為50.0
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: const Icon(Icons.person),
title: Text('$index'),
);
});
}
_buildSeparated() {
//下劃線widget預(yù)定義以供復(fù)用。
Widget divider1 = Divider(
color: Colors.blue,
);
Widget divider2 = Divider(color: Colors.green);
return ListView.separated(
scrollDirection: Axis.vertical,
//列表項(xiàng)構(gòu)造器
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
},
//分割器構(gòu)造器
separatorBuilder: (BuildContext context, int index) {
return index % 2 == 0 ? divider1 : divider2;
},
itemCount: 100);
}
_buildExtent() {
return ListView.builder(
prototypeItem: const ListTile(
title: Text('1'),
),
itemBuilder: (BuildContext context, int index) {
//LayoutLogPrint是一個(gè)自定義組件,在布局時(shí)可以打印當(dāng)前上下文中父組件給子組件的約束信息
return Center(child: Text('$index'),);
});
}
//無限加載列表
_buildInfinite(){
return ListView.separated(
itemBuilder: (context,index){
//如果到了表尾
if(_words[index] ==loadingTag) {
//如果數(shù)據(jù)不足100條
if (_words.length <= 100) {
//拉去數(shù)據(jù)
_retrieveData();
//加載顯示loading
return Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
child: const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2,),
),
);
} else {
//已經(jīng)加載100不再獲取數(shù)據(jù)
return Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(16),
child: const Text('沒有更多了',
style: TextStyle(color: Colors.grey),),
);
}
}
return ListTile(title: Text(_words[index]),);
},
separatorBuilder:(context,index)=>Divider(height:1,color: Colors.black,),
itemCount: _words.length);
}
void _retrieveData(){
Future.delayed(Duration(seconds: 5)).then((value){
setState(() {
_words.insertAll(_words.length-1,
//每次生成20個(gè)單詞
List.generate(20, (index){
return 'words $index';
}));
});
});
}
}
運(yùn)營(yíng)效果:

添加固定列表頭
很多時(shí)候我們需要給列表添加一個(gè)固定表頭,比如我們想實(shí)現(xiàn)一個(gè)商品列表,需要在列表頂部添加一個(gè)“商品列表”標(biāo)題,期望的效果如圖 6-6 所示:

我們按照之前經(jīng)驗(yàn),寫出如下代碼:
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(title:Text("商品列表")),
ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
]);
}
然后運(yùn)行,發(fā)現(xiàn)并沒有出現(xiàn)我們期望的效果,相反觸發(fā)了一個(gè)異常;
Vertical viewport was given unbounded height.
======== Exception caught by rendering library ===================================================== The following assertion was thrown during performResize(): Vertical viewport was given unbounded height. Viewports expand in the scrolling direction to fill their container. In this case, a vertical viewport was given an unlimited amount of vertical space in which to expand. This situation typically happens when a scrollable widget is nested inside another scrollable widget. If this widget is always nested in a scrollable widget there is no need to use a viewport because there will always be enough vertical space for the children. In this case, consider using a Column instead. Otherwise, consider using the "shrinkWrap" property (or a ShrinkWrappingViewport) to size the height of the viewport to the sum of the heights of its children.
從異常信息中我們可以看到是因?yàn)?code>ListView高度邊界無法確定引起,所以解決的辦法也很明顯,我們需要給ListView指定邊界,我們通過SizedBox指定一個(gè)列表高度看看是否生效:
Column(
children: [
ListTile(title: Text('商品列表'),),
SizedBox(height: 400,//指定高度
child: ListView.builder(itemBuilder: (BuildContext context,int index){
return ListTile(title: Text('$index'),);
}),
)
],
)

可以看到,現(xiàn)在沒有觸發(fā)異常并且列表已經(jīng)顯示出來了,但是我們的手機(jī)屏幕高度要大于 400,所以底部會(huì)有一些空白。那如果我們要實(shí)現(xiàn)列表鋪滿除表頭以外的屏幕空間應(yīng)該怎么做?直觀的方法是我們?nèi)?dòng)態(tài)計(jì)算,用屏幕高度減去狀態(tài)欄、導(dǎo)航欄、表頭的高度即為剩余屏幕高度,代碼如下:
... //省略無關(guān)代碼
SizedBox(
//Material設(shè)計(jì)規(guī)范中狀態(tài)欄、導(dǎo)航欄、ListTile高度分別為24、56、56
height: MediaQuery.of(context).size.height-24-56-56,
child: ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
)
...

可以看到,我們期望的效果實(shí)現(xiàn)了,但是這種方法并不優(yōu)雅,如果頁面布局發(fā)生變化,比如表頭布局調(diào)整導(dǎo)致表頭高度改變,那么剩余空間的高度就得重新計(jì)算。那么有什么方法可以自動(dòng)拉伸ListView以填充屏幕剩余空間的方法嗎?當(dāng)然有!答案就是Flex。在彈性布局中,可以使用Expanded自動(dòng)拉伸組件大小,并且我們也說過Column是繼承自Flex的,所以我們可以直接使用Column + Expanded來實(shí)現(xiàn),代碼如下:
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(title:Text("商品列表")),
Expanded(
child: ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
),
]);
}
運(yùn)行后,和上圖一樣,完美實(shí)現(xiàn)了!
總結(jié)
本節(jié)主要介紹了ListView 常用的的使用方式和要點(diǎn),但并沒有介紹ListView.custom方法,它需要實(shí)現(xiàn)一個(gè)SliverChildDelegate 用來給 ListView 生成列表項(xiàng)組件,更多詳情請(qǐng)參考 API 文檔。
demo完整代碼:gitee.com/wywinstonwy…
以上就是Flutter之 ListView組件使用示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Flutter之 ListView 組件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 重寫ViewGroup 分析onMeasure()和onLayout()方法
這篇文章主要介紹了Android 重寫ViewGroup 分析onMeasure()和onLayout()方法的相關(guān)資料,需要的朋友可以參考下2017-06-06
Android 使用Zbar實(shí)現(xiàn)掃一掃功能
這篇文章主要介紹了Android 使用Zbar實(shí)現(xiàn)掃一掃功能,本文用的是Zbar實(shí)現(xiàn)掃一掃,因?yàn)楦鶕?jù)本人對(duì)兩個(gè)庫的使用比較,發(fā)現(xiàn)Zbar解碼比Zxing速度要快,實(shí)現(xiàn)方式也簡(jiǎn)單,需要的朋友可以參考下2023-03-03
Android開發(fā)中一個(gè)簡(jiǎn)單實(shí)用的調(diào)試應(yīng)用技巧分享
這篇文章主要跟大家分享了一個(gè)簡(jiǎn)單實(shí)用的Android調(diào)試應(yīng)用技巧,文中介紹的非常詳細(xì),相信對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友下面來一起看看吧。2017-05-05
舉例講解Android應(yīng)用中SimpleAdapter簡(jiǎn)單適配器的使用
這篇文章主要介紹了Android應(yīng)用中SimpleAdapter簡(jiǎn)單適配器的使用例子,SimpleAdapter經(jīng)常在ListView被使用,需要的朋友可以參考下2016-04-04
Android如何實(shí)現(xiàn)接收和發(fā)送短信
這篇文章主要為大家詳細(xì)介紹了Android如何實(shí)現(xiàn)接收和發(fā)送短信,具有一定的實(shí)用性,感興趣的小伙伴們可以參考一下2016-08-08
Android DrawerLayout布局與NavigationView導(dǎo)航菜單應(yīng)用
這篇文章主要介紹了Android DrawerLayout抽屜布局與NavigationView導(dǎo)航菜單應(yīng)用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-01-01
Android?Studio實(shí)現(xiàn)簡(jiǎn)單頁面跳轉(zhuǎn)的詳細(xì)教程
這篇文章主要給大家介紹了關(guān)于Android?Studio實(shí)現(xiàn)簡(jiǎn)單頁面跳轉(zhuǎn)的詳細(xì)教程,文中通過圖文介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Android?Studio具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-01-01
Android中View.post和Handler.post的關(guān)系
這篇文章主要介紹了Android中View.post和Handler.post的關(guān)系,View.post和Handler.post是Android開發(fā)中經(jīng)常使用到的兩個(gè)”post“方法,關(guān)于兩者存在的區(qū)別與聯(lián)系,文章詳細(xì)分析需要的小伙伴可以參考一下2022-06-06
Android寫一個(gè)實(shí)時(shí)輸入框功能
這篇文章主要介紹了Android寫一個(gè)實(shí)時(shí)輸入框功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04

