[toc]
Flutter从入门到奔溃(四):撸一个包含列表刷新以及网络请求的首页
前记
我们之前粗略介绍了基础以及布局:
Flutter从入门到奔溃(一):撸一个登录界面
Flutter从入门到奔溃(二):撸一个个人界面
Flutter从入门到奔溃(三):撸一个App基础框架
都是属于比较简单的东西,而且也都是静态页面,这个速度实在是太慢了,我们开始加快速度吧!
这次我们来做一个首页,涉及到banner,list,上下拉刷新,以及网络请求。
分步实现
静态页面实现
页面分析
我们做下页面分析,其实页面可以划分为2个部分:
- 上部分的banner图展示
- 下部分的list展示
看过上一篇博文的朋友估计也知道,这里可以有2种实现方案了吧:
- CustomScrollView
- ListView
这里采用的是ListView的方式,有条件的朋友可以试下用CustomScrollView实现,如果看不懂,可以参考上一篇的博文。
代码实现
因为页面是需要状态更新的,所以我们可以使用StatefulWidget进行页面构建
StatefulWidget
// 资讯列表页面
class NewsListPage extends StatefulWidget {
@override
State createState() {
return new NewsListPageState();
}
}
State
而StatefulWidget的难点在于State,我们看下要怎么写:
class NewsListPageState extends State {
@override
Widget build(BuildContext context) {
...
}
}
我们在state的build方法里面返回我们构建好的一个ListViewwidget
item
而listView的精髓在于item的构建,我们看下item怎么构建:
return new Container(
child: new ListView.builder(
// 这里itemCount是将轮播图组件、分割线和列表items都作为ListView的item算了
itemCount: listData.length * 2 + 1,
controller: controller,
physics: physics,
itemBuilder: (context, i) => renderRow(i)));
},
层层递进,我们要在一个listview里面显示多种布局,那么必定要在rederRow()方法里面做手脚:
当index==0的时候,返回一个banner;
当index>0的时候,返回一个我们预先构建的item;
Widget renderRow(i) {
// i为0时渲染轮播图
if (i == 0) {
return new Container(
height: 180.0,
child: new BannerView(mWidgetsUtils.getBannerChild(slideData),
intervalDuration: const Duration(seconds: 3),
animationDuration: const Duration(milliseconds: 500)),
);
}
// i > 0时
i -= 1;
// i为奇数,渲染分割线
if (i.isOdd) {
return new Divider(height: 1.0);
}
// 将i取整
i = i ~/ 2;
// 得到列表item的数据
var itemData = listData[i];
// 代表列表item中的标题这一行
var titleRow = new Row(
children: [
// 标题充满一整行,所以用Expanded组件包裹
new Expanded(
child: new Text(itemData['title'], style: titleTextStyle),
)
],
);
// 时间这一行包含了作者头像、时间、评论数这几个
var timeRow = new Row(
children: [
// 这是作者头像,使用了圆形头像
new Container(
width: 20.0,
height: 20.0,
decoration: new BoxDecoration(
// 通过指定shape属性设置图片为圆形
shape: BoxShape.circle,
color: const Color(0xFFECECEC),
image: new DecorationImage(
image: new NetworkImage(itemData['authorImg']),
fit: BoxFit.cover),
border: new Border.all(
color: const Color(0xFFECECEC),
width: 2.0,
),
),
),
// 这是时间文本
new Padding(
padding: const EdgeInsets.fromLTRB(4.0, 0.0, 0.0, 0.0),
child: new Text(
itemData['timeStr'],
style: subtitleStyle,
),
),
// 这是评论数,评论数由一个评论图标和具体的评论数构成,所以是一个Row组件
new Expanded(
flex: 1,
child: new Row(
// 为了让评论数显示在最右侧,所以需要外面的Expanded和这里的MainAxisAlignment.end
mainAxisAlignment: MainAxisAlignment.end,
children: [
new Text("${itemData['commCount']}", style: subtitleStyle),
new Padding(
padding: new EdgeInsets.fromLTRB(4.0, 0.0, 0.0, 0.0),
child: new Image.asset('./images/ic_comment.png',
width: 16.0, height: 16.0),
)
],
),
)
],
);
var thumbImgUrl = itemData['thumb'];
// 这是item右侧的资讯图片,先设置一个默认的图片
var thumbImg = new Container(
margin: const EdgeInsets.all(10.0),
width: 60.0,
height: 60.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFECECEC),
image: new DecorationImage(
image: new ExactAssetImage('./images/ic_img_default.jpg'),
fit: BoxFit.cover),
border: new Border.all(
color: const Color(0xFFECECEC),
width: 2.0,
),
),
);
// 如果上面的thumbImgUrl不为空,就把之前thumbImg默认的图片替换成网络图片
if (thumbImgUrl != null && thumbImgUrl.length > 0) {
thumbImg = new Container(
margin: const EdgeInsets.all(10.0),
width: 60.0,
height: 60.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFECECEC),
image: new DecorationImage(
image: new NetworkImage(thumbImgUrl), fit: BoxFit.cover),
border: new Border.all(
color: const Color(0xFFECECEC),
width: 2.0,
),
),
);
}
// 这里的row代表了一个ListItem的一行
var row = new Row(
children: [
// 左边是标题,时间,评论数等信息
new Expanded(
flex: 1,
child: new Padding(
padding: const EdgeInsets.all(10.0),
child: new Column(
children: [
titleRow,
new Padding(
padding: const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 0.0),
child: timeRow,
)
],
),
),
),
// 右边是资讯图片
new Padding(
padding: const EdgeInsets.all(6.0),
child: new Container(
width: 100.0,
height: 80.0,
color: const Color(0xFFECECEC),
child: new Center(
child: thumbImg,
),
),
)
],
);
// 用InkWell包裹row,让row可以点击
return new InkWell(
child: row,
onTap: () {},
);
}
同样的,我们使用InkWell进行包裹,以便于点击事件的获取。
插件使用
但是,banner我们只能用简单的TabBarView实现,下拉我们可以用RefreshIndicator来实现,甚至可以封装很多功能出来,自动滑,无限滑,上拉~
但是可能会更倾向于使用第三方插件来拓展功能,而不用自己去实现...(233333...其实我想自己搞的,折腾了一个晚上写得都不满意)
这里推荐一个插件库,我们需要可以去找对应的插件使用:
插件库
这里举一个使用的例子,给flutter加上shared_preferences持久化存储的插件
-
打开上述网站,找到对应的插件,查看依赖方式
打开项目中的pubspec.yaml文件,在dependencies节点下添加依赖
执行右上角的pageages get或者在命令行中执行flutter packages get.
使用吧!
网络请求
我觉得一个app分为3个阶段:
- 静态页面
- 接口联调
- bug修复
- 运营增量(大部分情况并不关我们开发人员的事情)
而其中的2与3都与网络请求有关。
所以我们来 简单说下flutter的网络请求吧,这里只是简单使用,并不会涉及到很多封装,由俭入奢慢慢来吧。
简单介绍
flutter自带有http库,我们目前只需要简单使用到即可,
比如说我要请求下百度:
- 引入对应的import
import 'package:http/http.dart' as http;
- 编写测试方法
void testNet(){
http.get('http://www.baidu.com').then((res){
debugPrint("test get method ,and the res is ${res.body.toString()}");
});
}
-
测试运行
简单封装
我们对封装(勉为其难称为封装)来个概述:
- 考虑get post ,其他delete,put先不考虑
- get的话参数拼在url后面(app内其实可以不用考虑get的参数长度问题),post直接使用丢body
- 简单点用
async await Future简单介绍
做安卓的时候,所有耗时任务都推荐放在子线程中去处理,为的就是不影响ui线程(主线程)的页面渲染工作。
但是fultter是单线程的,也就是说,无论你是网络请求,数据处理,页面渲染,都是在同一个线程里面,那怎么保障页面渲染不会anr呢?
flutter推出了async ,它是一个延迟计算的标志,标志了把这个任务放到了延迟运算的队列(await)中,通过Future进行返回。
具体参考 参考
代码封装
import 'package:http/http.dart' as http;
import 'dart:async';
class Http {
// get 请求
static Future get(String url, {Map params}) async {
if (params != null && params.isNotEmpty) {
StringBuffer sb = new StringBuffer("?");
params.forEach((key, value) {
sb.write("$key" + "=$value" + "&");
});
String paramStr = sb.toString();
paramStr = paramStr.substring(0, paramStr.length - 1);
url += paramStr;
}
http.Response res = await http.get(url);
if(res.statusCode==200){
return res.body;
}else{
return null;
}
}
// post请求
static Future post(String url, {Map params}) async {
http.Response res = await http.post(url, body: params);
return res.body;
}
}
简单使用
getNewsList(int curPage) {
String url = Api.NEWS_LIST_BASE_URL;
url += '?pageIndex=$curPage&pageSize=4';
Http.get(url).then((res) {
if (res != null) {
Map map = json.decode(res);
debugPrint("the res is" + map.toString());
if (map['code'] == 0) {
var msg = map['msg'];
listTotalSize = msg['news']['total'];
var _listData = msg['news']['data'];
var _slideData = msg['slide'];
setState(() {
if (curPage == 1) {
listData = _listData;
slideData = _slideData;
} else {
List tempList = new List();
tempList.addAll(listData);
tempList.addAll(_listData);
if (tempList.length >= listTotalSize) {
tempList.add('the end');
}
listData = tempList;
slideData = _slideData;
}
});
}
} else {
debugPrint("the res is null");
}
});
}
因为在请求类那里进行了判断是否为200,所以可能会返回null,判空是必要的,在请求那里可以进行吐司或者interface进行回调。
上下拉刷新
下拉刷新可以使用RefreshIndicator,但是并不支持上拉...
所以~我们可以用插件来进行实现。
flutter_refresh : ^0.0.1
有了插件,使用就很方便了:
return new Refresh(
onFooterRefresh: onFooterRefresh,
onHeaderRefresh: onHeaderRefresh,
childBuilder: (BuildContext context,
{ScrollController controller, ScrollPhysics physics}) {
return new Container(
child: new ListView.builder(
// 这里itemCount是将轮播图组件、分割线和列表items都作为ListView的item算了
itemCount: listData.length * 2 + 1,
controller: controller,
physics: physics,
itemBuilder: (context, i) => renderRow(i)));
},
);
上下拉的刷新方法为:
Future onFooterRefresh() {
return new Future.delayed(new Duration(seconds: 2), () {
setState(() {
_mCurPage++;
getNewsList(_mCurPage);
});
});
}
Future onHeaderRefresh() {
return new Future.delayed(new Duration(seconds: 2), () {
setState(() {
_mCurPage = 1;
getNewsList(_mCurPage);
});
});
}
总结
至此,页面已经搭建完成,上下拉和网络请求也已经粗糙地搭建完成了。
互勉!