## ListView
ListView是最常用的可滾動widget,它可以沿一個方向線性排布所有子widget。我們看看ListView的默認構造函數定義:
```
ListView({
...
//可滾動widget公共參數
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
EdgeInsetsGeometry padding,
//ListView各個構造函數的共同參數
double itemExtent,
bool shrinkWrap = false,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double cacheExtent,
//子widget列表
List<Widget> children = const <Widget>[],
})
```
上面參數分為兩組:第一組是可滾動widget公共參數,前面已經介紹過,不再贅述;第二組是ListView各個構造函數(ListView有多個構造函數)的共同參數,我們重點來看看這些參數,:
- itemExtent:該參數如果不為null,則會強制children的"長度"為itemExtent的值;這里的"長度"是指滾動方向上子widget的長度,即如果滾動方向是垂直方向,則itemExtent代表子widget的高度,如果滾動方向為水平方向,則itemExtent代表子widget的長度。在ListView中,指定itemExtent比讓子widget自己決定自身長度會更高效,這是因為指定itemExtent后,滾動系統可以提前知道列表的長度,而不是總是動態去計算,尤其是在滾動位置頻繁變化時(滾動系統需要頻繁去計算列表高度)。
- shrinkWrap:該屬性表示是否根據子widget的總長度來設置ListView的長度,默認值為`false` 。默認情況下,ListView的會在滾動方向盡可能多的占用空間。當ListView在一個無邊界(滾動方向上)的容器中時,shrinkWrap必須為`true`。
- addAutomaticKeepAlives:該屬性表示是否將列表項(子widget)包裹在AutomaticKeepAlive widget中;典型地,在一個懶加載列表中,如果將列表項包裹在AutomaticKeepAlive中,在該列表項滑出視口時該列表項不會被GC,它會使用KeepAliveNotification來保存其狀態。如果列表項自己維護其KeepAlive狀態,那么此參數必須置為`false`。
- addRepaintBoundaries:該屬性表示是否將列表項(子widget)包裹在RepaintBoundary中。當可滾動widget滾動時,將列表項包裹在RepaintBoundary中可以避免列表項重繪,但是當列表項重繪的開銷非常小(如一個顏色塊,或者一個較短的文本)時,不添加RepaintBoundary反而會更高效。和addAutomaticKeepAlive一樣,如果列表項自己維護其KeepAlive狀態,那么此參數必須置為`false`。
> 注意:上面這些參數并非ListView特有,在本章后面介紹的其它可滾動widget也可能會擁有這些參數,它們的含義是相同的。
### 默認構造函數
默認構造函數有一個`children`參數,它接受一個Widget列表(List)。這種方式適合只有少量的子widget的情況,因為這種方式需要將所有`children`都提前創建好(這需要做大量工作),而不是等到子widget真正顯示的時候再創建。實際上通過此方式創建的ListView和使用SingleChildScrollView+Column的方式沒有本質的區別。下面是一個例子:
```
ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
const Text('I\'m dedicating every day to you'),
const Text('Domestic life was never quite my style'),
const Text('When you smile, you knock me out, I fall apart'),
const Text('And I thought I was so smart'),
],
);
```
> 注意:可滾動widget通過一個List來作為其children屬性時,只適用于子widget較少的情況,這是一個通用規律,并非ListView自己的特性,像GridView也是如此。
### ListView.builder
`ListView.builder`適合列表項比較多(或者無限)的情況,因為只有當子Widget真正顯示的時候才會被創建。下面看一下ListView.builder的核心參數列表:
```
ListView.builder({
// ListView公共參數已省略
...
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
...
})
```
- itemBuilder:它是列表項的構建器,類型為IndexedWidgetBuilder,返回值為一個widget。當列表滾動到具體的index位置時,會調用該構建器構建列表項。
- itemCount:列表項的數量,如果為null,則為無限列表。
看一個例子:
```
ListView.builder(
itemCount: 100,
itemExtent: 50.0, //強制高度為50.0
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}
);
```

### ListView.separated
`ListView.separated`可以生成列表項之間的分割器,它除了比`ListView.builder`多了一個`separatorBuilder`參數,該參數是一個分割器生成器。下面我們看一個例子:奇數行添加一條藍色下劃線,偶數行添加一條綠色下劃線。
```
class ListView3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
//下劃線widget預定義以供復用。
Widget divider1=Divider(color: Colors.blue,);
Widget divider2=Divider(color: Colors.green);
return ListView.separated(
itemCount: 100,
//列表項構造器
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
},
//分割器構造器
separatorBuilder: (BuildContext context, int index) {
return index%2==0?divider1:divider2;
},
);
}
}
```

### 實例:無限加載列表
假設我們要從數據源異步分批拉取一些數據,然后用ListView顯示,當我們滑動到列表末尾時,判斷是否需要再去拉取數據,如果是,則去拉取,拉取過程中在表尾顯示一個loading,拉取成功后將數據插入列表;如果不需要再去拉取,則在表尾提示"沒有更多"。代碼如下:
```
class InfiniteListView extends StatefulWidget {
@override
_InfiniteListViewState createState() => new _InfiniteListViewState();
}
class _InfiniteListViewState extends State<InfiniteListView> {
static const loadingTag = "##loading##"; //表尾標記
var _words = <String>[loadingTag];
@override
void initState() {
_retrieveData();
}
@override
Widget build(BuildContext context) {
return ListView.separated(
itemCount: _words.length,
itemBuilder: (context, index) {
//如果到了表尾
if (_words[index] == loadingTag) {
//不足100條,繼續獲取數據
if (_words.length - 1 < 100) {
//獲取數據
_retrieveData();
//加載時顯示loading
return Container(
padding: const EdgeInsets.all(16.0),
alignment: Alignment.center,
child: SizedBox(
width: 24.0,
height: 24.0,
child: CircularProgressIndicator(strokeWidth: 2.0)
),
);
} else {
//已經加載了100條數據,不再獲取數據。
return Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16.0),
child: Text("沒有更多了", style: TextStyle(color: Colors.grey),)
);
}
}
//顯示單詞列表項
return ListTile(title: Text(_words[index]));
},
separatorBuilder: (context, index) => Divider(height: .0),
);
}
void _retrieveData() {
Future.delayed(Duration(seconds: 2)).then((e) {
_words.insertAll(_words.length - 1,
//每次生成20個單詞
generateWordPairs().take(20).map((e) => e.asPascalCase).toList()
);
setState(() {
//重新構建列表
});
});
}
}
```

代碼比較簡單,讀者可以參照代碼中的注釋理解,故不再贅述。需要說明的是,`_retrieveData()`的功能是模擬從數據源異步獲取數據,我們使用english\_words包的`generateWordPairs()`方法每次生成20個單詞。
### 添加固定表頭
很多時候我們需要給列表添加一個固定表頭,比如我們想實現一個商品列表,需要在列表頂部添加一個“商品列表”標題,效果如下:

我們按照之前經驗,寫出如下代碼:
```
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(title:Text("商品列表")),
ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
]);
}
```
然后運行,發現并沒有出現我們期望的效果,相反觸發了一個異常;
```
Error caught by rendering library, thrown during performResize()。
Vertical viewport was given unbounded height ...
```
從異常信息中我們可到是因為ListView高度邊界無法確定引起,所以解決的辦法也很明顯,我們需要給ListView指定邊界,我們通過`SizedBox`指定一個列表高度看看是否生效:
```
... //省略無關代碼
SizedBox(
height: 400, //指定列表高度為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。前面已經介紹過在Flex布局中,可以使用Expanded自動拉伸組件大小的Widget,我們也說過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生成列表項widget,更多詳情請參考API文檔。
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示