Flutter之 ListView組件使用示例詳解
ListView的默認構造函數定義
ListView
是最常用的可滾動組件之一,它可以沿一個方向線性排佈所有子組件,並且它也支持列表項懶加載(在需要時才會創建)。我們看看ListView的默認構造函數定義:
ListView({ ... //可滾動widget公共參數 Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController? controller, bool? primary, ScrollPhysics? physics, EdgeInsetsGeometry? padding, //ListView各個構造函數的共同參數 double? itemExtent, Widget? prototypeItem, //列表項原型,後面解釋 bool shrinkWrap = false, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, double? cacheExtent, // 預渲染區域長度 //子widget列表 List<Widget> children = const <Widget>[], })
上面參數分為兩組:第一組是可滾動組件的公共參數;第二組是ListView
各個構造函數ListView
有多個構造函數的共同參數,我們重點來看看這些參數,:
- itemExtent:該參數如果不為null,則會強制children的“長度”為itemExtent的值;這裡的“長度”是指滾動方向上子組件的長度,也就是說如果滾動方向是垂直方向,則itemExtent代表子組件的高度;如果滾動方向為水平方向,則itemExtent就代表子組件的寬度。在ListView中,指定itemExtent比讓子組件自己決定自身長度會有更好的性能,這是因為指定itemExtent後,滾動系統可以提前知道列表的長度,而無需每次構建子組件時都去再計算一下,尤其是在滾動位置頻繁變化時(滾動系統需要頻繁去計算列表高度)。
- prototypeItem:如果我們知道列表中的所有列表項長度都相同但不知道具體是多少,這時我們可以指定一個列表項,該列表項被稱為 prototypeItem(列表項原型)。指定 prototypeItem 後,可滾動組件會在 layout 時計算一次它延主軸方向的長度,這樣也就預先知道瞭所有列表項的延主軸方向的長度,所以和指定 itemExtent 一樣,指定 prototypeItem 會有更好的性能。註意,itemExtent 和prototypeItem 互斥,不能同時指定它們。
shrinkWrap
:該屬性表示是否根據子組件的總長度來設置ListView
的長度,默認值為false
。默認情況下,ListView
的會在滾動方向盡可能多的占用空間。當ListView
在一個無邊界(滾動方向上)的容器中時,shrinkWrap
必須為true
。- addRepaintBoundaries:該屬性表示是否將列表項(子組件)包裹在RepaintBoundary組件中。RepaintBoundary 讀者可以先簡單理解為它是一個”繪制邊界“,將列表項包裹在RepaintBoundary中可以避免列表項不必要的重繪,但是當列表項重繪的開銷非常小(如一個顏色塊,或者一個較短的文本)時,不添加RepaintBoundary反而會更高效。如果列表項自身來維護是否需要添加繪制邊界組件,則此參數應該指定為 false。
註意:上面這些參數並非ListView
特有,其它可滾動組件也可能會擁有這些參數,它們的含義是相同的。
默認構造函數
默認構造函數有一個children
參數,它接受一個Widget列表(List)。這種方式適合隻有少量的子組件數量已知且比較少的情況,反之則應該使用ListView.builder
按需動態構建列表項。
註意,雖然這種方式將所有children
一次性傳遞給 ListView,但子組件)仍然是在需要時才會加載(build(如有)、佈局、繪制),也就是說通過默認構造函數構建的 ListView 也是基於 Sliver 的列表懶加載模型。
下面是一個例子:
可以看到,雖然使用默認構造函數創建的列表也是懶加載的,但我們還是需要提前將 Widget 創建好,等到真正需要加載的時候才會對 Widget 進行佈局和繪制。
shrinkWrap: true 效果,ListView根據子視圖計算高度:
shrinkWrap: false的效果,ListView
的會在滾動方向盡可能多的占用空間。
ListView.builder
ListView.builder
適合列表項比較多或者列表項不確定的情況,下面看一下ListView.builder
的核心參數列表
ListView.builder({ // ListView公共參數已省略 ... required IndexedWidgetBuilder itemBuilder, int itemCount, ... })
itemBuilder
:它是列表項的構建器,類型為IndexedWidgetBuilder
,返回值為一個widget。當列表滾動到具體的index
位置時,會調用該構建器構建列表項。
itemCount
:列表項的數量,如果為null
,則為無限列表。
下面看一個例子:
return ListView.builder( itemCount: 100, itemExtent: 50,//強制高度為50.0 itemBuilder: (BuildContext context,int index){ return ListTile( leading: const Icon(Icons.person), title: Text('$index'), ); });
運行效果“
ListView.separated
ListView.separated
可以在生成的列表項之間添加一個分割組件,它比ListView.builder
多瞭一個separatorBuilder
參數,該參數是一個分割組件生成器。
下面我們看一個例子:奇數行添加一條藍色下劃線,偶數行添加一條綠色下劃線。
//下劃線widget預定義以供復用。 Widget divider1=Divider(color: Colors.blue,); Widget divider2=Divider(color: Colors.green); return ListView.separated( //列表項構造器 itemBuilder: (BuildContext context, int index) { return ListTile(title: Text("$index")); }, //分割器構造器 separatorBuilder: (BuildContext context, int index) { return index%2==0?divider1:divider2; }, itemCount: 100); }
運行效果:
固定高度列表
前面說過,給列表指定 itemExtent
或 prototypeItem
會有更高的性能,所以當我們知道列表項的高度都相同時,強烈建議指定 itemExtent
或 prototypeItem
。
下面看一個示例:
ListView.builder( prototypeItem: const ListTile( title: Text('1'), ), itemBuilder: (BuildContext context, int index) { return Center(child: Text('$index'),); });
因為列表項都是一個 ListTile,高度相同,但是我們不知道 ListTile 的高度是多少,所以指定瞭prototypeItem
,每個item高度根據prototypeItem來定。
ListView 原理
ListView 內部組合瞭 Scrollable、Viewport 和 Sliver,需要註意:
- ListView 中的列表項組件都是 RenderBox,並不是 Sliver, 這個一定要註意。
- 一個 ListView 中隻有一個Sliver,對列表項進行按需加載的邏輯是 Sliver 中實現的。
- ListView 的 Sliver 默認是 SliverList,如果指定瞭 itemExtent ,則會使用 SliverFixedExtentList;如果 prototypeItem 屬性不為空,則會使用 SliverPrototypeExtentList,無論是是哪個,都實現瞭子組件的按需加載模型。
實例:無限加載列表
假設我們要從數據源異步分批拉取一些數據,然後用ListView
展示,當我們滑動到列表末尾時,判斷是否需要再去拉取數據,如果是,則去拉取,拉取過程中在表尾顯示一個loading,拉取成功後將數據插入列表;如果不需要再去拉取,則在表尾提示"沒有更多"。
代碼如下:
class MyListViewPage extends StatefulWidget { const MyListViewPage({Key? key}) : super(key: key); @override _MyListViewPageState createState() => _MyListViewPageState(); } class _MyListViewPageState extends State<MyListViewPage> { static const loadingTag = "##loading##"; //表尾標記 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, //強制高度為50.0 itemBuilder: (BuildContext context, int index) { return ListTile( leading: const Icon(Icons.person), title: Text('$index'), ); }); } _buildSeparated() { //下劃線widget預定義以供復用。 Widget divider1 = Divider( color: Colors.blue, ); Widget divider2 = Divider(color: Colors.green); return ListView.separated( scrollDirection: Axis.vertical, //列表項構造器 itemBuilder: (BuildContext context, int index) { return ListTile(title: Text("$index")); }, //分割器構造器 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是一個自定義組件,在佈局時可以打印當前上下文中父組件給子組件的約束信息 return Center(child: Text('$index'),); }); } //無限加載列表 _buildInfinite(){ return ListView.separated( itemBuilder: (context,index){ //如果到瞭表尾 if(_words[index] ==loadingTag) { //如果數據不足100條 if (_words.length <= 100) { //拉去數據 _retrieveData(); //加載顯示loading return Container( padding: const EdgeInsets.all(16), alignment: Alignment.center, child: const SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2,), ), ); } else { //已經加載100不再獲取數據 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個單詞 List.generate(20, (index){ return 'words $index'; })); }); }); } }
運營效果:
添加固定列表頭
很多時候我們需要給列表添加一個固定表頭,比如我們想實現一個商品列表,需要在列表頂部添加一個“商品列表”標題,期望的效果如圖 6-6 所示:
我們按照之前經驗,寫出如下代碼:
@override Widget build(BuildContext context) { return Column(children: <Widget>[ ListTile(title:Text("商品列表")), ListView.builder(itemBuilder: (BuildContext context, int index) { return ListTile(title: Text("$index")); }), ]); }
然後運行,發現並沒有出現我們期望的效果,相反觸發瞭一個異常;
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.
從異常信息中我們可以看到是因為ListView
高度邊界無法確定引起,所以解決的辦法也很明顯,我們需要給ListView
指定邊界,我們通過SizedBox
指定一個列表高度看看是否生效:
Column( children: [ ListTile(title: Text('商品列表'),), SizedBox(height: 400,//指定高度 child: ListView.builder(itemBuilder: (BuildContext context,int index){ return ListTile(title: Text('$index'),); }), ) ], )
可以看到,現在沒有觸發異常並且列表已經顯示出來瞭,但是我們的手機屏幕高度要大於 400,所以底部會有一些空白。那如果我們要實現列表鋪滿除表頭以外的屏幕空間應該怎麼做?直觀的方法是我們去動態計算,用屏幕高度減去狀態欄、導航欄、表頭的高度即為剩餘屏幕高度,代碼如下:
... //省略無關代碼 SizedBox( //Material設計規范中狀態欄、導航欄、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")); }), ) ...
可以看到,我們期望的效果實現瞭,但是這種方法並不優雅,如果頁面佈局發生變化,比如表頭佈局調整導致表頭高度改變,那麼剩餘空間的高度就得重新計算。那麼有什麼方法可以自動拉伸ListView以填充屏幕剩餘空間的方法嗎?當然有!答案就是Flex。在彈性佈局中,可以使用Expanded自動拉伸組件大小,並且我們也說過Column是繼承自Flex的,所以我們可以直接使用Column + Expanded來實現,代碼如下:
@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")); }), ), ]); }
運行後,和上圖一樣,完美實現瞭!
總結
本節主要介紹瞭ListView
常用的的使用方式和要點,但並沒有介紹ListView.custom
方法,它需要實現一個SliverChildDelegate
用來給 ListView 生成列表項組件,更多詳情請參考 API 文檔。
demo完整代碼:gitee.com/wywinstonwy…
以上就是Flutter之 ListView組件使用示例詳解的詳細內容,更多關於Flutter之 ListView 組件的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Android ListView列表優化的方法詳解
- Flutter Sliver滾動組件的演示代碼
- Flutter實現自定義篩選框的示例代碼
- Flutter listview如何實現下拉刷新上拉加載更多功能
- flutter實現一個列表下拉抽屜的示例代碼