Flutter實(shí)戰(zhàn) ListView

2021-03-08 11:27 更新

ListView是最常用的可滾動(dòng)組件之一,它可以沿一個(gè)方向線性排布所有子組件,并且它也支持基于 Sliver 的延遲構(gòu)建模型。我們看看 ListView 的默認(rèn)構(gòu)造函數(shù)定義:

  1. ListView({
  2. ...
  3. //可滾動(dòng)widget公共參數(shù)
  4. Axis scrollDirection = Axis.vertical,
  5. bool reverse = false,
  6. ScrollController controller,
  7. bool primary,
  8. ScrollPhysics physics,
  9. EdgeInsetsGeometry padding,
  10. //ListView各個(gè)構(gòu)造函數(shù)的共同參數(shù)
  11. double itemExtent,
  12. bool shrinkWrap = false,
  13. bool addAutomaticKeepAlives = true,
  14. bool addRepaintBoundaries = true,
  15. double cacheExtent,
  16. //子widget列表
  17. List<Widget> children = const <Widget>[],
  18. })

上面參數(shù)分為兩組:第一組是可滾動(dòng)組件的公共參數(shù),本章第一節(jié)中已經(jīng)介紹過(guò),不再贅述;第二組是ListView各個(gè)構(gòu)造函數(shù)(ListView有多個(gè)構(gòu)造函數(shù))的共同參數(shù),我們重點(diǎn)來(lái)看看這些參數(shù),:

  • itemExtent:該參數(shù)如果不為null,則會(huì)強(qiáng)制children的“長(zhǎng)度”為itemExtent的值;這里的“長(zhǎng)度”是指滾動(dòng)方向上子組件的長(zhǎng)度,也就是說(shuō)如果滾動(dòng)方向是垂直方向,則itemExtent代表子組件的高度;如果滾動(dòng)方向?yàn)樗椒较颍瑒titemExtent就代表子組件的寬度。在ListView中,指定itemExtent比讓子組件自己決定自身長(zhǎng)度會(huì)更高效,這是因?yàn)橹付?code>itemExtent后,滾動(dòng)系統(tǒng)可以提前知道列表的長(zhǎng)度,而無(wú)需每次構(gòu)建子組件時(shí)都去再計(jì)算一下,尤其是在滾動(dòng)位置頻繁變化時(shí)(滾動(dòng)系統(tǒng)需要頻繁去計(jì)算列表高度)。
  • shrinkWrap:該屬性表示是否根據(jù)子組件的總長(zhǎng)度來(lái)設(shè)置ListView的長(zhǎng)度,默認(rèn)值為false 。默認(rèn)情況下,ListView的會(huì)在滾動(dòng)方向盡可能多的占用空間。當(dāng)ListView在一個(gè)無(wú)邊界(滾動(dòng)方向上)的容器中時(shí),shrinkWrap必須為true。
  • addAutomaticKeepAlives:該屬性表示是否將列表項(xiàng)(子組件)包裹在AutomaticKeepAlive 組件中;典型地,在一個(gè)懶加載列表中,如果將列表項(xiàng)包裹在AutomaticKeepAlive中,在該列表項(xiàng)滑出視口時(shí)它也不會(huì)被GC(垃圾回收),它會(huì)使用KeepAliveNotification來(lái)保存其狀態(tài)。如果列表項(xiàng)自己維護(hù)其KeepAlive狀態(tài),那么此參數(shù)必須置為false。
  • addRepaintBoundaries:該屬性表示是否將列表項(xiàng)(子組件)包裹在RepaintBoundary組件中。當(dāng)可滾動(dòng)組件滾動(dòng)時(shí),將列表項(xiàng)包裹在RepaintBoundary中可以避免列表項(xiàng)重繪,但是當(dāng)列表項(xiàng)重繪的開(kāi)銷非常?。ㄈ缫粋€(gè)顏色塊,或者一個(gè)較短的文本)時(shí),不添加RepaintBoundary反而會(huì)更高效。和addAutomaticKeepAlive一樣,如果列表項(xiàng)自己維護(hù)其KeepAlive狀態(tài),那么此參數(shù)必須置為false。

注意:上面這些參數(shù)并非ListView特有,在本章后面介紹的其它可滾動(dòng)組件也可能會(huì)擁有這些參數(shù),它們的含義是相同的。

#默認(rèn)構(gòu)造函數(shù)

默認(rèn)構(gòu)造函數(shù)有一個(gè)children參數(shù),它接受一個(gè) Widget 列表(List)。這種方式適合只有少量的子組件的情況,因?yàn)檫@種方式需要將所有children都提前創(chuàng)建好(這需要做大量工作),而不是等到子 widget 真正顯示的時(shí)候再創(chuàng)建,也就是說(shuō)通過(guò)默認(rèn)構(gòu)造函數(shù)構(gòu)建的 ListView 沒(méi)有應(yīng)用基于 Sliver 的懶加載模型。實(shí)際上通過(guò)此方式創(chuàng)建的ListView和使用SingleChildScrollView+Column的方式?jīng)]有本質(zhì)的區(qū)別。下面是一個(gè)例子:

  1. ListView(
  2. shrinkWrap: true,
  3. padding: const EdgeInsets.all(20.0),
  4. children: <Widget>[
  5. const Text('I\'m dedicating every day to you'),
  6. const Text('Domestic life was never quite my style'),
  7. const Text('When you smile, you knock me out, I fall apart'),
  8. const Text('And I thought I was so smart'),
  9. ],
  10. );

再次強(qiáng)調(diào),可滾動(dòng)組件通過(guò)一個(gè) List 來(lái)作為其 children 屬性時(shí),只適用于子組件較少的情況,這是一個(gè)通用規(guī)律,并非ListView自己的特性,像GridView也是如此。

#ListView.builder

ListView.builder適合列表項(xiàng)比較多(或者無(wú)限)的情況,因?yàn)橹挥挟?dāng)子組件真正顯示的時(shí)候才會(huì)被創(chuàng)建,也就說(shuō)通過(guò)該構(gòu)造函數(shù)創(chuàng)建的ListView是支持基于 Sliver 的懶加載模型的。下面看一下ListView.builder的核心參數(shù)列表:

  1. ListView.builder({
  2. // ListView公共參數(shù)已省略
  3. ...
  4. @required IndexedWidgetBuilder itemBuilder,
  5. int itemCount,
  6. ...
  7. })

  • 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,則為無(wú)限列表。

可滾動(dòng)組件的構(gòu)造函數(shù)如果需要一個(gè)列表項(xiàng) Builder,那么通過(guò)該構(gòu)造函數(shù)構(gòu)建的可滾動(dòng)組件通常就是支持基于 Sliver 的懶加載模型的,反之則不支持,這是個(gè)一般規(guī)律。我們?cè)诤竺嬖诮榻B可滾動(dòng)組件的構(gòu)造函數(shù)時(shí)將不再專門說(shuō)明其是否支持基于 Sliver 的懶加載模型了。

下面看一個(gè)例子:

  1. ListView.builder(
  2. itemCount: 100,
  3. itemExtent: 50.0, //強(qiáng)制高度為50.0
  4. itemBuilder: (BuildContext context, int index) {
  5. return ListTile(title: Text("$index"));
  6. }
  7. );

運(yùn)行效果如圖6-2所示:

圖6-2

#ListView.separated

ListView.separated可以在生成的列表項(xiàng)之間添加一個(gè)分割組件,它比ListView.builder多了一個(gè)separatorBuilder參數(shù),該參數(shù)是一個(gè)分割組件生成器。

下面我們看一個(gè)例子:奇數(shù)行添加一條藍(lán)色下劃線,偶數(shù)行添加一條綠色下劃線。

  1. class ListView3 extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. //下劃線widget預(yù)定義以供復(fù)用。
  5. Widget divider1=Divider(color: Colors.blue,);
  6. Widget divider2=Divider(color: Colors.green);
  7. return ListView.separated(
  8. itemCount: 100,
  9. //列表項(xiàng)構(gòu)造器
  10. itemBuilder: (BuildContext context, int index) {
  11. return ListTile(title: Text("$index"));
  12. },
  13. //分割器構(gòu)造器
  14. separatorBuilder: (BuildContext context, int index) {
  15. return index%2==0?divider1:divider2;
  16. },
  17. );
  18. }
  19. }

圖6-3

#實(shí)例:無(wú)限加載列表

假設(shè)我們要從數(shù)據(jù)源異步分批拉取一些數(shù)據(jù),然后用ListView展示,當(dāng)我們滑動(dòng)到列表末尾時(shí),判斷是否需要再去拉取數(shù)據(jù),如果是,則去拉取,拉取過(guò)程中在表尾顯示一個(gè) loading,拉取成功后將數(shù)據(jù)插入列表;如果不需要再去拉取,則在表尾提示"沒(méi)有更多"。代碼如下:

  1. class InfiniteListView extends StatefulWidget {
  2. @override
  3. _InfiniteListViewState createState() => new _InfiniteListViewState();
  4. }
  5. class _InfiniteListViewState extends State<InfiniteListView> {
  6. static const loadingTag = "##loading##"; //表尾標(biāo)記
  7. var _words = <String>[loadingTag];
  8. @override
  9. void initState() {
  10. super.initState();
  11. _retrieveData();
  12. }
  13. @override
  14. Widget build(BuildContext context) {
  15. return ListView.separated(
  16. itemCount: _words.length,
  17. itemBuilder: (context, index) {
  18. //如果到了表尾
  19. if (_words[index] == loadingTag) {
  20. //不足100條,繼續(xù)獲取數(shù)據(jù)
  21. if (_words.length - 1 < 100) {
  22. //獲取數(shù)據(jù)
  23. _retrieveData();
  24. //加載時(shí)顯示loading
  25. return Container(
  26. padding: const EdgeInsets.all(16.0),
  27. alignment: Alignment.center,
  28. child: SizedBox(
  29. width: 24.0,
  30. height: 24.0,
  31. child: CircularProgressIndicator(strokeWidth: 2.0)
  32. ),
  33. );
  34. } else {
  35. //已經(jīng)加載了100條數(shù)據(jù),不再獲取數(shù)據(jù)。
  36. return Container(
  37. alignment: Alignment.center,
  38. padding: EdgeInsets.all(16.0),
  39. child: Text("沒(méi)有更多了", style: TextStyle(color: Colors.grey),)
  40. );
  41. }
  42. }
  43. //顯示單詞列表項(xiàng)
  44. return ListTile(title: Text(_words[index]));
  45. },
  46. separatorBuilder: (context, index) => Divider(height: .0),
  47. );
  48. }
  49. void _retrieveData() {
  50. Future.delayed(Duration(seconds: 2)).then((e) {
  51. setState(() {
  52. //重新構(gòu)建列表
  53. _words.insertAll(_words.length - 1,
  54. //每次生成20個(gè)單詞
  55. generateWordPairs().take(20).map((e) => e.asPascalCase).toList()
  56. );
  57. });
  58. });
  59. }
  60. }

運(yùn)行后效果如圖6-4、6-5所示:

圖6-4圖6-5

代碼比較簡(jiǎn)單,讀者可以參照代碼中的注釋理解,故不再贅述。需要說(shuō)明的是,_retrieveData()的功能是模擬從數(shù)據(jù)源異步獲取數(shù)據(jù),我們使用 english_words 包的generateWordPairs()方法每次生成20個(gè)單詞。

#添加固定列表頭

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

圖6-6

我們按照之前經(jīng)驗(yàn),寫出如下代碼:

  1. @override
  2. Widget build(BuildContext context) {
  3. return Column(children: <Widget>[
  4. ListTile(title:Text("商品列表")),
  5. ListView.builder(itemBuilder: (BuildContext context, int index) {
  6. return ListTile(title: Text("$index"));
  7. }),
  8. ]);
  9. }

然后運(yùn)行,發(fā)現(xiàn)并沒(méi)有出現(xiàn)我們期望的效果,相反觸發(fā)了一個(gè)異常;

  1. Error caught by rendering library, thrown during performResize()。
  2. Vertical viewport was given unbounded height ...

從異常信息中我們可以看到是因?yàn)?code>ListView高度邊界無(wú)法確定引起,所以解決的辦法也很明顯,我們需要給ListView指定邊界,我們通過(guò)SizedBox指定一個(gè)列表高度看看是否生效:

  1. ... //省略無(wú)關(guān)代碼
  2. SizedBox(
  3. height: 400, //指定列表高度為400
  4. child: ListView.builder(itemBuilder: (BuildContext context, int index) {
  5. return ListTile(title: Text("$index"));
  6. }),
  7. ),
  8. ...

運(yùn)行效果如圖6-7所示:

圖6-7

可以看到,現(xiàn)在沒(méi)有觸發(fā)異常并且列表已經(jīng)顯示出來(lái)了,但是我們的手機(jī)屏幕高度要大于400,所以底部會(huì)有一些空白。那如果我們要實(shí)現(xiàn)列表鋪滿除表頭以外的屏幕空間應(yīng)該怎么做?直觀的方法是我們?nèi)?dòng)態(tài)計(jì)算,用屏幕高度減去狀態(tài)欄、導(dǎo)航欄、表頭的高度即為剩余屏幕高度,代碼如下:

  1. ... //省略無(wú)關(guān)代碼
  2. SizedBox(
  3. //Material設(shè)計(jì)規(guī)范中狀態(tài)欄、導(dǎo)航欄、ListTile高度分別為24、56、56
  4. height: MediaQuery.of(context).size.height-24-56-56,
  5. child: ListView.builder(itemBuilder: (BuildContext context, int index) {
  6. return ListTile(title: Text("$index"));
  7. }),
  8. )
  9. ...

運(yùn)行效果如下圖6-8所示:

圖6-8

可以看到,我們期望的效果實(shí)現(xiàn)了,但是這種方法并不優(yōu)雅,如果頁(yè)面布局發(fā)生變化,比如表頭布局調(diào)整導(dǎo)致表頭高度改變,那么剩余空間的高度就得重新計(jì)算。那么有什么方法可以自動(dòng)拉伸ListView以填充屏幕剩余空間的方法嗎?當(dāng)然有!答案就是Flex。前面已經(jīng)介紹過(guò)在彈性布局中,可以使用Expanded自動(dòng)拉伸組件大小,并且我們也說(shuō)過(guò)Column是繼承自Flex的,所以我們可以直接使用Column+Expanded來(lái)實(shí)現(xiàn),代碼如下:

  1. @override
  2. Widget build(BuildContext context) {
  3. return Column(children: <Widget>[
  4. ListTile(title:Text("商品列表")),
  5. Expanded(
  6. child: ListView.builder(itemBuilder: (BuildContext context, int index) {
  7. return ListTile(title: Text("$index"));
  8. }),
  9. ),
  10. ]);
  11. }

運(yùn)行后,和上圖一樣,完美實(shí)現(xiàn)了!

#總結(jié)

本節(jié)主要介紹了ListView的一些公共參數(shù)以及常用的構(gòu)造函數(shù)。不同的構(gòu)造函數(shù)對(duì)應(yīng)了不同的列表項(xiàng)生成模型,如果需要自定義列表項(xiàng)生成模型,可以通過(guò)ListView.custom來(lái)自定義,它需要實(shí)現(xiàn)一個(gè)SliverChildDelegate用來(lái)給 ListView 生成列表項(xiàng)組件,更多詳情請(qǐng)參考 API 文檔。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)