上期實現了一個網絡輪播圖的效果,自定義了一個輪播圖組件,繼承自StatefulWidget,我們知道Flutter中並沒有像Android中activity的概念。頁面見的跳轉是通過路由從一個全屏組件跳轉到另外的一個全屏組件,那如果我想在A組件中更新B組件的數據應該怎么實現呢?
今天我們來實現一個支持篩選的列表頁面。前面我們已經實現來一個支持下拉刷新和上拉加載更多的列表組件,這里就不在做更多介紹來,效果圖如下:
通過點擊左滑菜單篩選列表的數據。由於列表在之前的一篇文章已經說明過Flutter學習四之實現一個支持刷新加載的列表,所以這里就直接上列表的代碼:我們將列表定義程一個組件,方便,頁面引用,列表的請求也放在組件內部。
class ArticleListBaseWidget extends StatefulWidget {
int cid = 0; //文章分類的id
@override
State<StatefulWidget> createState() {
return ArticleListBaseState();
}
ArticleListBaseWidget({this.cid});
}
class ArticleListBaseState extends State<ArticleListBaseWidget> {
RefreshController refreshController =
RefreshController(initialRefresh: true);
int pageNum = 0;
List<TopArticleBeanData> _articles = [];
void onRefresh() {
pageNum = 0;
getArticle(true, getApiName());
}
void loadMore() {
pageNum++;
getArticle(false, getApiName());
}
//根據cid參數來處理apiName,如果沒有傳cid參數進來表示不支持分類篩選。
String getApiName() {
String apiName = "";
if (widget.cid > 0) {
apiName = "article/list/$pageNum/json?cid=${widget.cid}";
} else {
apiName = "article/list/$pageNum/json";
}
print("apiName=$apiName");
return apiName;
}
///置頂文章
void getArticle(bool isRefresh, String apiName) async {
///文章接口請求
dio.get(apiName).then((value) {
///文章實體解析
ArticleListEntityEntity articleBeanEntity =
ArticleListEntityEntity().fromJson(jsonDecode(value.toString()));
if (isRefresh) {
_articles = articleBeanEntity.data.datas;
} else {
_articles.addAll(articleBeanEntity.data.datas);
}
///接口調用成功后更新數據需要調用setState()方法
setState(() {
});
if (articleBeanEntity.data.datas.length ==0) {
//如果接口沒有數據返回
if (isRefresh) {
//如果是下拉刷新的時候並且接口沒有返回數據,隱藏列表控件,展示空頁面
refreshController.refreshFailed();
} else {
//如果是上啦加載更多並且沒有返回數據,就展示列表控件,但是下拉提示沒有更多數據了
refreshController.loadNoData();
}
} else {
//如果有數據返回,展示列表控件
if (isRefresh) {
//如果是下拉刷新,並且有數據返回正常展示頁面數據
refreshController.refreshCompleted();
} else {
//如果是上啦加載,並且有數據返回正常展示頁面數據
refreshController.loadComplete();
}
}
}).catchError((onError) {
if (isRefresh) {
//如果下拉刷新,並且接口出錯,展示錯誤頁面
refreshController.refreshFailed();
} else {
//如果上拉加載更多,並且接口出錯,展示列表控件,底部下拉位置展示加載失敗
refreshController.loadFailed();
}
});
}
@override
Widget build(BuildContext context) {
return
SmartRefresher(
controller: refreshController,
enablePullUp: true,
onRefresh: onRefresh,
onLoading: loadMore,
header: WaterDropHeader(),
footer: ClassicFooter(),
child:
ListView.builder(
itemBuilder: (context, index) => ItemPage(_articles[index]),
itemCount: _articles.length)
);
}
}
因為要實現的列表支持篩選項,所以我們這里要在構造方法中接收一個cid參數,用來進行列表篩選的,代碼很簡單,主要是定義來一個下拉刷新和上拉加載更多要執行的方法,然后在接口返回中針對返回的數據處理來對應的刷新狀態,以及對異常的處理,注釋已經寫的很清楚來。
接下來我們新建一個頁面,
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("文章列表")),
endDrawer: drawerSystem(),
body:ArticleListBaseWidget(key:key,cid: cid));
}
這里endDrawer方法是用來添加一個左滑菜單欄的,這里我們用來顯示需要篩選的分類數據組件drawerSystem,該組件是由兩個聯動的列表組成的,同時在構造方法中,傳人兩個列表的點擊時間,方便調用該組件的頁面進行數據更新。這里要注意的是獲取狀態欄的高度和appbar的高度,還有我們要在endDrawer中自定義一個標題欄,但是endDrawer頁面默認會有一個距離頂部的空白高度,我們要利用MediaQuery.removePadding方法來去掉頂部留白的部分,然后設置自己定義的標題欄。這里的appbar高度用常量kToolbarHeight來獲取,MediaQuery.of(context).padding.top就是狀態欄的高度,看到往上很多是直接寫死,這樣在不同的機型上面展示的效果會不一樣的。
具體代碼:
Widget drawerSystem() {
return MediaQuery.removePadding(
context: context,
removeTop: true,
child: Container(
color: Colors.blue,
width: 320,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
child: Text("體系數據"),
height: kToolbarHeight + MediaQuery.of(context).padding.top,
//appbar高度+狀態欄高度
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top),
alignment: Alignment.center,
color: Colors.grey[200]),
Expanded(
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Container(
child: ListView.separated(
separatorBuilder: (context, index) {
return Divider(
height: 1, color: Colors.grey[50]);
},
itemBuilder: (context, index) => ItemPageSystem(
screenList[index],
null,
index,
(index) => {onItemClick(index)}),
itemCount: screenList.length),
color: Colors.white)),
Expanded(
child: Container(
child: ListView.separated(
separatorBuilder: (context, index) {
return Divider(
height: 1, color: Colors.white);
},
itemBuilder: (context, index) => ItemPageSystem(
null,
screenChildList[index],
index,
(index) => onItemClickChild(index)),
itemCount: screenChildList.length),
color: Colors.grey[50]))
],
))
],
)));
}
ItemPageSystem是listView每該item的樣式,可以自己定義,這里要注意的一下,就是listview item的點擊事件,這里為來方便在父組件進行數據的處理,所以是從item組件的構造方法將方法傳進去的,
ItemPageSystem(
this.screenDataBean, this.screenChild, this.index, this.function);
然后就可以在點擊事件處理篩選的聯動效果了,
//一級篩選點擊事件處理
onItemClick(int index) {
setState(() {
//全局記住點擊位置
this.index = index;
//設置二級菜單數據集合
screenChildList = screenList[index].children;
//遍歷一級數據設置一級菜單標示,是否選中
updateListSelect(index, screenList);
});
}
//二級篩選事件處理
onItemClickChild(int index) {
setState(() {
//全局記住二級菜單點擊位置
indexChild = index;
//雙層循環遍歷清空二級菜單為非選中狀態
for (int i = 0; i < screenList.length; i++) {
updateListSelect(-1, screenList[i].children);
}
//設置當前點擊數據為選中狀態
updateListSelect(index, screenChildList);
cid = screenChildList[index].id;
Navigator.pop(context);//關閉側邊菜單欄
key.currentState.refreshController.requestRefresh();
});
}
到這一步基本已經完成了,我們看下效果發現,點擊了篩選后並不能更新列表數據,這是為什么呢,查找了半天不知道問題處在哪里,通過查閱文檔發現,如果想在父組件里面更新繼承自StatefulWidget的組件的數據,光設置setStat還是不行,因為組件的Key是相同的沒有改變,所以要想在父組件更新StatefulWidget組件的數據,需要用到GlobalKey,GlobalKey 能夠跨 Widget 訪問狀態,簡單來說就是可以通過GlobalKey來調用子組件的方法或者屬性。
具體用法:
//在子組件的構造方法中添加一個Key參數。並且調用super方法返回
ArticleListBaseWidget({Key key,this.cid}):super(key:key);
//在父組件中初始化組件,ArticleListBaseState為你要更改狀態的子組件
final GlobalKey<ArticleListBaseState> key = GlobalKey<ArticleListBaseState>();
//在父組件中初始化子組件的位置,將GlobalKey對象傳回到子組件
ArticleListBaseWidget(key:key,cid: cid));
//父組件中,在你需要更新子組件的位置利用GlobalKey對象調用子組件的方法
key.currentState.refreshController.requestRefresh();
在此運行發現,可以通過篩選條件來更新列表了。