1. 前言
Flutter
作為時下最流行的技術之一,憑藉其出色的效能以及抹平多端的差異優勢,早已引起大批技術愛好者的關注,甚至一些閒魚
,美團
,騰訊
等大公司均已投入生產使用。雖然目前其生態還沒有完全成熟,但身靠背後的Google
加持,其發展速度已經足夠驚人,可以預見將來對Flutter
開發人員的需求也會隨之增長。
無論是為了技術嚐鮮還是以後可能的工作機會,都9102年了,作為一個前端開發者,似乎沒有理由不去嘗試它。正是帶著這樣的心理,筆者也開始學習Flutter
,同時建了一個用於練習的倉庫,後續所有程式碼都會託管在上面,歡迎star,一起學習。
在上一篇文章中,我們學習了Flutter
中使用頻率最高的一些基礎元件。但是在一些場景中,當元件的寬度或高度超出螢幕邊緣時,Flutter
往往會給出overflow
警告,提醒有元件溢位螢幕。為了解決這個問題,今天我們就來學習最常用的一個滾動型元件
— ListView元件
。
2. ListView使用方法
從功能比較上來看,Flutter
中的ListView
元件和RN
中的ScrollView
/FlatList
非常相似,但是在使用方法上還是有點區別。接下來,就跟著我一起來看看ListView
元件都有哪些常用的使用方法。
2.1 ListView()
第一種使用方法就是直接呼叫其預設建構函式
來建立列表,效果等同於RN
中的ScrollView
元件。但是這種方式建立的列表存在一個問題:對於那些長列表
或者需要較昂貴渲染開銷
的子元件,即使還沒有出現在螢幕中但仍然會被ListView
所建立,這將是一項較大的開銷,使用不當可能引起效能問題甚至卡頓。
不過話說回來,雖然該方法可能會有效能問題,但還是取決於其不同的應用場景而定。下面我們就來看下其建構函式(已省略不常用屬性):
ListView({
Axis scrollDirection = Axis.vertical,
ScrollController controller,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
double cacheExtent,
List<Widget> children = const <Widget>[],
})
複製程式碼
scrollDirection
: 列表的滾動方向,可選值有Axis
的horizontal
和vertical
,可以看到預設是垂直方向上滾動;controller
: 控制器,與列表滾動相關,比如監聽列表的滾動事件;physics
: 列表滾動至邊緣後繼續拖動的物理效果,Android
與iOS
效果不同。Android
會呈現出一個波紋狀(對應ClampingScrollPhysics
),而iOS
上有一個回彈的彈性效果(對應BouncingScrollPhysics
)。如果你想不同的平臺上呈現各自的效果可以使用AlwaysScrollableScrollPhysics
,它會根據不同平臺自動選用各自的物理效果。如果你想禁用在邊緣的拖動效果,那可以使用NeverScrollableScrollPhysics
;shrinkWrap
: 該屬性將決定列表的長度是否僅包裹其內容的長度。當ListView
嵌在一個無限長的容器元件中時,shrinkWrap
必須為true,否則Flutter
會給出警告;padding
: 列表內邊距;itemExtent
: 子元素長度。當列表中的每一項長度是固定的情況下可以指定該值,有助於提高列表的效能(因為它可以幫助ListView
在未實際渲染子元素之前就計算出每一項元素的位置);cacheExtent
: 預渲染區域長度,ListView
會在其可視區域的兩邊留一個cacheExtent
長度的區域作為預渲染區域(對於ListView.build
或ListView.separated
建構函式建立的列表,不在可視區域和預渲染區域內的子元素不會被建立或會被銷燬);children
: 容納子元素的元件陣列。
上面的屬性介紹一大堆,都不如一個實際例子來得實在。我們可以用一個ListView
元件來包裹上篇文章中實現的銀行卡,寵物卡片,朋友圈這三個例子:
程式碼(檔案地址)
class NormalList extends StatelessWidget {
const NormalList({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
CreditCard(data: creditCardData),
PetCard(data: petCardData),
FriendCircle(data: friendCircleData),
],
);
}
}
複製程式碼
預覽
可以看到,預設建構函式
的用法非常之簡單,直接把子元素元件放在children
陣列中就可以了。但是潛在的問題前面也已經解釋過,對於長列表
這種應用場景還是應該用ListView.build
建構函式效能會更好。
2.2 ListView.build()
ListView
預設建構函式雖使用簡單,但不適用於長列表。為此,我們來看下ListView.build
建構函式:
ListView.builder({
...
int itemCount,
@required IndexedWidgetBuilder itemBuilder,
})
複製程式碼
這裡省略了不常用以及和ListView
預設建構函式重複的一些引數,相比之下我們可以發現ListView.builder
多了兩個新的引數:
itemCount
: 列表中元素的數量;itemBuilder
: 子元素的渲染方法,允許自定義子元素元件(等同於rn
中FlatList
元件的renderItem
屬性)。
不同於ListView
預設建構函式通過children
引數指定子元素的這種方式,ListView.build
通過暴露統一的itemBuilder
方法將渲染子元素的控制權交還給呼叫方。這裡我們用一個微信公眾號的例子來說明ListView.build
的使用方法(公眾號卡片的樣式佈局可以看這裡,也算是對基礎元件的一個鞏固和複習):
程式碼(檔案地址)
class SubscribeAccountList extends StatelessWidget {
const SubscribeAccountList({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: Color(0xFFEFEFEF),
child: ListView.builder(
itemCount: subscribeAccountList.length,
itemBuilder: (context, index) {
return SubscribeAccountCard(data: subscribeAccountList[index]);
},
),
);
}
}
複製程式碼
預覽
根據上面的程式碼可以看到,ListView.build
建立列表最重要的兩個引數就是itemCount
和itemBuilder
。對於公眾號列表這個例子,由於每個公眾號訊息卡片的佈局都是有規則的,而且這個列表的數量可能非常之多,所以用ListView.build
來建立再適合不過了。
2.3 ListView.separated()
絕大多數列表類的需求我們都可以用ListView.build
建構函式來解決問題,不過有的列表子項之間需要分割線
,此時我們可以用Flutter
提供的另一個建構函式ListView.separated
來建立列表。來看下其建構函式有什麼不同:
ListView.separated({
...
@required IndexedWidgetBuilder separatorBuilder
})
複製程式碼
相比於ListView.build
建構函式,可以看到ListView.separated
僅僅是多了一個separatorBuilder
必填引數。顧名思義,這就是暴露給呼叫方自定義分割線元件的回撥方法。以支付寶的好友列表為例(好友卡片的樣式佈局可以看這裡),我們來看下ListView.separated
的使用方法:
程式碼(檔案地址)
class FriendList extends StatelessWidget {
const FriendList({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.separated(
itemCount: friendListData.length,
itemBuilder: (context, index) {
return FriendCard(data: friendListData[index]);
},
separatorBuilder: (context, index) {
return Divider(
height: .5,
indent: 75,
color: Color(0xFFDDDDDD),
);
},
);
}
}
複製程式碼
預覽
看程式碼可以知道不同點就在於實現了separatorBuilder
這個函式,通過它我們可以自定義每個子元素之間的分割線了。
2.4 小結
到目前為止,我們一共學習了ListView
,ListView.build
和ListView.separated
三種建立列表的方式,它們各自都有其適用的場景,所以遇到需求時還是得具體問題具體分析。
不過,其實ListView
還有一個建構函式:ListView.custom
。而且ListView.build
和ListView.separated
最終都是通過ListView.custom
實現的。但是本文並不打算介紹這種方法,因為一般情況下前面提到的三種構造方法就已經足夠解決問題了(以後遇到實際問題再研究這個)。
3. ListView進階方法
上文我們介紹了ListView
的基礎用法,但是在實際的產品中,我們還會遇到列表下拉重新整理
和上拉載入
等需求。接下來,就讓我們學習下Flutter
中應該如何實現此類互動操作。
3.1 下拉重新整理
要在Flutter
中實現列表的下拉重新整理效果,其實非常簡單,因為Flutter
給我們封裝好了一個RefreshIndicator
元件,使用起來也非常方便。看下示例程式碼:
class PullDownRefreshList extends StatefulWidget {
const PullDownRefreshList({Key key}) : super(key: key);
@override
_PullDownRefreshListState createState() => _PullDownRefreshListState();
}
class _PullDownRefreshListState extends State<PullDownRefreshList> {
Future onRefresh() {
return Future.delayed(Duration(seconds: 1), () {
Toast.show('當前已是最新資料', context);
});
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: this.onRefresh,
child: ListView.separated(
itemCount: friendListData.length,
itemBuilder: (context, index) {
return FriendCard(data: friendListData[index]);
},
separatorBuilder: (context, index) {
return Divider(
height: .5,
indent: 75,
color: Color(0xFFDDDDDD),
);
},
),
);
}
}
複製程式碼
由於列表的資料來源是可變的,因此這次的元件我們選用繼承自StatefulWidget
。
可以看到RefreshIndicator
的用法十分簡單,只要將我們原來的ListView
作為其child,並且實現其onRefresh
方法就好了。而onRefresh
方法其實是重新整理完畢通知RefreshIndicator
的一個回撥函式。上述程式碼中,我們模擬了一個1s的等待當做網路請求,然後彈出一個Toast提示"已經是最新資料"(此處的Toast
是安裝了toast: ^0.1.3
這個包,Flutter
原生並沒有提供)。
這裡模仿了今日頭條的列表UI作為示例(新聞卡片的樣式佈局可以看這裡),我們來看下效果:
可以看到一切都如預期成功執行了,效果還是不錯的,而且RefreshIndicator
使用起來也是非常簡便。但是,由於Flutter
封裝好的RefreshIndicator
元件可定製性有點弱,不太能夠滿足大多數app中自定義樣式的要求。不過好在看了下RefreshIndicator
的原始碼並不是很多,等日後學了動畫再回頭來研究下如何定製一個自定義的下拉重新整理元件。
3.2 上拉載入
除了下拉重新整理之外,上拉載入是經常會遇到的另一種列表操作。不過,這次Flutter
倒是沒有像下拉重新整理那樣提供現成的元件可以直接呼叫,上拉載入的互動需要我們自己完成。為此,我們先來簡單分析下:
- 元件內部需要一個
list
變數儲存當前列表的資料來源; - 元件內部需要一個
bool
型的isLoading
標誌位來表示當前是否處於Loading
狀態; - 需要能夠判斷出當前列表是否已經滾動到底部,而這就要藉助到我們前面提到過的
controller
屬性了(ScrollController
可以獲取到當前列表的滾動位置以及列表最大滾動區域,相比較即可得到結果); - 當開始載入資料的時候,需要將
isLoading
置為true
;當資料載入完畢的時候,需要將新的資料合併到list
變數中,並且重新將isLoading
置為false
。
根據上面的思路,我們可以得到下面的程式碼:
class PullUpLoadMoreList extends StatefulWidget {
const PullUpLoadMoreList({Key key}) : super(key: key);
@override
_PullUpLoadMoreListState createState() => _PullUpLoadMoreListState();
}
class _PullUpLoadMoreListState extends State<PullUpLoadMoreList> {
bool isLoading = false;
ScrollController scrollController = ScrollController();
List<NewsViewModel> list = List.from(newsList);
@override
void initState() {
super.initState();
// 給列表滾動新增監聽
this.scrollController.addListener(() {
// 滑動到底部的關鍵判斷
if (
!this.isLoading &&
this.scrollController.position.pixels >= this.scrollController.position.maxScrollExtent
) {
// 開始載入資料
setState(() {
this.isLoading = true;
this.loadMoreData();
});
}
});
}
@override
void dispose() {
// 元件銷燬時,釋放資源(一定不能忘,否則可能會引起記憶體洩露)
super.dispose();
this.scrollController.dispose();
}
Future loadMoreData() {
return Future.delayed(Duration(seconds: 1), () {
setState(() {
this.isLoading = false;
this.list.addAll(newsList);
});
});
}
Widget renderBottom() {
// TODO
}
@override
Widget build(BuildContext context) {
return ListView.separated(
controller: this.scrollController,
itemCount: this.list.length + 1,
separatorBuilder: (context, index) {
return Divider(height: .5, color: Color(0xFFDDDDDD));
},
itemBuilder: (context, index) {
if (index < this.list.length) {
return NewsCard(data: this.list[index]);
} else {
return this.renderBottom();
}
},
);
}
}
複製程式碼
其中有一點需要注意,列表的itemCount
值變成了list.length + 1
,這是因為我們多渲染了一個底部元件
。當不在載入的時候,我們可以展示一個上拉載入更多
的提示性元件;當正在載入資料時,我們又可以展示一個努力載入中...
的佔位元件。renderBottom
的實現如下:
Widget renderBottom() {
if(this.isLoading) {
return Container(
padding: EdgeInsets.symmetric(vertical: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'努力載入中...',
style: TextStyle(
fontSize: 15,
color: Color(0xFF333333),
),
),
Padding(padding: EdgeInsets.only(left: 10)),
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 3),
),
],
),
);
} else {
return Container(
padding: EdgeInsets.symmetric(vertical: 15),
alignment: Alignment.center,
child: Text(
'上拉載入更多',
style: TextStyle(
fontSize: 15,
color: Color(0xFF333333),
),
),
);
}
}
複製程式碼
最後,我們再來看下最終的實現效果:
4. 總結
首先,本文介紹了常用的ListView
,ListView.build
和ListView.separated
三種構造方法來建立列表,並結合實際的例子加以說明其不同的使用場景。緊接著,又介紹了列表元件下拉重新整理
和上拉載入
這兩個較常用到的互動操作在Flutter
中應該如何實現。
通過文中的5個實際例子,相信你一定已經對Flutter
中如何使用ListView
有了初步瞭解,剩下的就是多練習(盤它)咯~