[toc]
# Flutter从入门到奔溃(四):撸一个包含列表刷新以及网络请求的首页
# 前记
我们之前粗略介绍了基础以及布局:
[Flutter从入门到奔溃(一):撸一个登录界面](Flutter从入门到奔溃(一):撸一个登录界面)
[Flutter从入门到奔溃(二):撸一个个人界面](Flutter从入门到奔溃(二):撸一个个人界面)
[Flutter从入门到奔溃(三):撸一个App基础框架](Flutter从入门到奔溃(三):撸一个App基础框架)
都是属于比较简单的东西,而且也都是静态页面,这个速度实在是太慢了,我们开始加快速度吧!
这次我们来做一个首页,涉及到banner,list,上下拉刷新,以及网络请求。
# 分步实现
## 静态页面实现
### 页面分析
我们做下页面分析,其实页面可以划分为2个部分:
* 上部分的banner图展示
* 下部分的list展示
看过上一篇博文的朋友估计也知道,这里可以有2种实现方案了吧:
* CustomScrollView
* ListView
这里采用的是ListView的方式,有条件的朋友可以试下用CustomScrollView实现,如果看不懂,可以参考上一篇的博文。
### 代码实现
因为页面是需要状态更新的,所以我们可以使用**StatefulWidget**进行页面构建
#### StatefulWidget
“`dart
// 资讯列表页面
class NewsListPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new NewsListPageState();
}
}
“`
#### State
而**StatefulWidget**的难点在于*State*,我们看下要怎么写:
“`dart
class NewsListPageState extends State<NewsListPage> {
@override
Widget build(BuildContext context) {
…
}
}
“`
我们在*state*的*build*方法里面返回我们构建好的一个**ListView**widget
#### item
而listView的精髓在于item的构建,我们看下item怎么构建:
“`dart
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;
“`dart
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: <Widget>[
// 标题充满一整行,所以用Expanded组件包裹
new Expanded(
child: new Text(itemData[‘title’], style: titleTextStyle),
)
],
);
// 时间这一行包含了作者头像、时间、评论数这几个
var timeRow = new Row(
children: <Widget>[
// 这是作者头像,使用了圆形头像
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: <Widget>[
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: <Widget>[
// 左边是标题,时间,评论数等信息
new Expanded(
flex: 1,
child: new Padding(
padding: const EdgeInsets.all(10.0),
child: new Column(
children: <Widget>[
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…其实我想自己搞的,折腾了一个晚上写得都不满意)
这里推荐一个插件库,我们需要可以去找对应的插件使用:
[插件库](Dart Packages)
这里举一个使用的例子,给flutter加上*shared_preferences*持久化存储的插件
1. 打开上述网站,找到对应的插件,查看依赖方式
2. 打开项目中的**pubspec.yaml**文件,在*dependencies*节点下添加依赖
3. 执行右上角的*pageages get*或者在命令行中执行*flutter packages get*.
4. 使用吧!
## 网络请求
我觉得一个app分为3个阶段:
1. 静态页面
2. 接口联调
3. bug修复
4. 运营增量(大部分情况并不关我们开发人员的事情)
而其中的**2**与**3**都与网络请求有关。
所以我们来 简单说下flutter的网络请求吧,这里只是简单使用,并不会涉及到很多封装,由俭入奢慢慢来吧。
### 简单介绍
flutter自带有http库,我们目前只需要简单使用到即可,
比如说我要请求下百度:
1. 引入对应的import
“`dart
import ‘package:http/http.dart’ as http;
“`
2. 编写测试方法
“`dart
void testNet(){
http.get(‘百度一下,你就知道‘).then((res){
debugPrint(“test get method ,and the res is ${res.body.toString()}”);
});
}
“`
3. 测试运行
### 简单封装
我们对封装(勉为其难称为封装)来个概述:
1. 考虑get post ,其他delete,put先不考虑
2. get的话参数拼在url后面(app内其实可以不用考虑get的参数长度问题),post直接使用丢body
3. 简单点用
#### async await Future简单介绍
做安卓的时候,所有耗时任务都推荐放在子线程中去处理,为的就是不影响ui线程(主线程)的页面渲染工作。
但是fultter是单线程的,也就是说,无论你是网络请求,数据处理,页面渲染,都是在同一个线程里面,那怎么保障页面渲染不会anr呢?
flutter推出了**async **,它是一个*延迟计算*的标志,标志了把这个任务放到了*延迟运算的队列(await)*中,通过*Future*进行返回。
具体参考 [参考](人类身份验证 – SegmentFault)
#### 代码封装
“`dart
import ‘package:http/http.dart’ as http;
import ‘dart:async’;
class Http {
// get 请求
static Future<String> get(String url, {Map<String, String> 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<String> post(String url, {Map<String, String> params}) async {
http.Response res = await http.post(url, body: params);
return res.body;
}
}
“`
#### 简单使用
“`dart
getNewsList(int curPage) {
String url = Api.NEWS_LIST_BASE_URL;
url += ‘?pageIndex=$curPage&pageSize=4’;
Http.get(url).then((res) {
if (res != null) {
Map<String, dynamic> 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*,但是并不支持上拉…
所以~我们可以用插件来进行实现。
“`dart
flutter_refresh : ^0.0.1
“`
有了插件,使用就很方便了:
“`dart
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)));
},
);
“`
上下拉的刷新方法为:
“`dart
Future<Null> onFooterRefresh() {
return new Future.delayed(new Duration(seconds: 2), () {
setState(() {
_mCurPage++;
getNewsList(_mCurPage);
});
});
}
Future<Null> onHeaderRefresh() {
return new Future.delayed(new Duration(seconds: 2), () {
setState(() {
_mCurPage = 1;
getNewsList(_mCurPage);
});
});
}
“`
# 总结
至此,页面已经搭建完成,上下拉和网络请求也已经粗糙地搭建完成了。
互勉!