沸点 UI & 功能 编写(上)
Nealyang opened this issue · comments
介绍
这一章节代码量可能会比较大,我们将完成沸点的UI以及相应功能编写,完成此篇,你将得到如下的界面效果:
数据准备
由于之前的首页编写中已经介绍了关于本地数据model的使用,这里我们将直接使用线上数据来进行我们的代码编写
通过掘金web版的公开api我们可以知道沸点的请求api地址
- lib/api.dart
// 沸点
static const String PINS_LIST = 'https://short-msg-ms.juejin.im/v1/pinList/recommend';
从沸点的每一个cell中,我们需要去分析构成该UI,大致需要的字段,通常,在我们的项目开发中,这些也是与开发约束的。
通过分析,我们可以看到沸点的每一个cell分为两种,文字+图片 以及文字+链接的形式,当然,其中每一个沸点也可能没有图片,也有的沸点包含主题、文字中含有链接。这些,在我们定义 沸点的数据model的时候都应该包含进去,所以如下,我们提取我们需要字段
- lib/model/pins_cell.dart
Map<String, dynamic> user;
String objectId;
String uid;
String content;
List<String> pictures;
int commentCount;
int likedCount;
String createdAt;
Map<String, dynamic> topic;
String url;
String urlTitle;
String urlPic;
在数据model中,应该包含他的构造函数以及factory中对请求数据的处理
factory PinsCell.fromJson(Map<String, dynamic> json) {
Map<String, dynamic> user = new Map();
user['avatarLarge'] = json['user']['avatarLarge'];
user['objectId'] = json['user']['objectId'];
user['company'] = json['user']['company'];
user['jobTitle'] = json['user']['jobTitle'];
user['role'] = json['user']['role'];
user['userName'] = json['user']['username'];
user['currentUserFollowed'] = json['user']['currentUserFollowed'];
Map<String, dynamic> topic = new Map();
// 有的沸点没有topic
if (json['topic'] != null) {
topic['objectId'] = json['topic']['objectId'];
topic['title'] = json['topic']['title'];
}
List<String> pics = new List();
// pics = json['pictures'];_TypeError (type 'List<dynamic>' is not a subtype of type 'List<String>')
json['pictures'].forEach((ele) {
pics.add(ele);
});
return PinsCell(
commentCount: json['commentCount'],
content: json['content'],
createdAt: Util.getTimeDuration(json['createdAt']),
likedCount: json['likedCount'],
objectId: json['objectId'],
pictures: pics,
topic: topic,
uid: json['uid'],
url: json['url'],
urlPic: json['urlPic'],
urlTitle: json['urlTitle'],
user: user);
}
- 注意上面代码中关于Map和list数据类型的处理,这里我们是不能够直接复制的,否则会出现
_TypeError (type 'List<dynamic>' is not a subtype of type 'List<String>')
的错误,也就是数据类型转换的问题。 所以对于Map以及List的数据类型,这里我们单独拿出来通过遍历来重新赋值的。 - 关于沸点的topic字段,有些沸点是不存在的,所以这里我们需要加一层判断,否则在直接取值的时候会报错。当然,这个注意项可能更加的设计到业务一些
定义好数据model后,我们去编写请求方法
- lib/util/data_util.dart
// 沸点 列表
static Future<List<PinsCell>> getPinsListData(
Map<String, dynamic> params) async {
List<PinsCell> resultList = new List();
var response = await NetUtils.get(Api.PINS_LIST, params: params);
var responseList = response['d']['list'];
for (int i = 0; i < responseList.length; i++) {
PinsCell pinsCell;
try {
pinsCell = PinsCell.fromJson(responseList[i]);
} catch (e) {
print("error $e at $i");
continue;
}
resultList.add(pinsCell);
}
return resultList;
}
- 数据请求同样适用我们在net_util.dart下封装的get和post方法
- 拿到数据后根据数据结构获取列表数据封装成我们的Pins model
编写沸点页面UI
沸点页面,我们需要一些变量阿里存储页面信息,比如沸点list、请求参数、翻页等
- lib/pages/pins_page.dart
List<PinsCell> _listData = new List();
Map<String, dynamic> _params = {
"src": 'web',
"uid": "",
"limit": 20,
"device_id": "",
"token": ""
};
bool _isRequesting = false; //是否正在请求数据的flag
bool _hasMore = true;
String before = '';
ScrollController _scrollController = new ScrollController();
编写相关的请求方法,然后在页面初始化的时候调用,
void getPinsList(bool isLoadMore) {
if (_isRequesting || !_hasMore) return;
if (before != '') {
_params['before'] = before;
}
if (!isLoadMore) {
_params['before'] = '';
}
_isRequesting = true;
before = DateTime.now().toString().replaceFirst(RegExp(r' '), 'T') + 'Z';
DataUtils.getPinsListData(_params).then((resultData) {
List<PinsCell> resultList = new List();
if (isLoadMore) {
resultList.addAll(_listData);
}
resultList.addAll(resultData);
if (this.mounted) {
setState(() {
_listData = resultList;
_hasMore = resultData.length != 0;
_isRequesting = false;
});
}
});
}
- 当页面正在请求、以及当前页已经是最后一页的时候,不进行请求
- 里面的before字段是掘金web版请求网络数据翻页的字段,这里跟业务相关,我们可以不去关心
- 使用我们之前在dataUtil中封装的请求方法,在获取请求数据后,如果是loadMore,则需要将之前list数据叠加,否则为直接赋值。同时需要设置页面的 isRequesting hasMore字段
- 注意这里我们setState之前判断了页面的mounted,因为在页面退出时我们需要销毁Controller,而请求是异步操作,所以如果我们在页面已经销毁的时候进行setState操作,页面会报错。
@override
Widget build(BuildContext context) {
if (_listData.length > 0) {
return Container(
color: Color(0xFFF4F5F5),
child: ListView.builder(
itemCount: _listData.length + 1,
itemBuilder: _itemBuilder,
controller: _scrollController,
),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
}
build方法中比较简单,其实就是初始化一个列表。这里再强调下,使用ListView.builder去实现长列表是非常好的选择,其性能也是非常的优越,会进行一些数据回收工作。
在initState的时候,我们进行一些页面的请求和Controller的初始化工作
@override
void initState() {
getPinsList(false);
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
print('loadMore');
getPinsList(true);
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
页面完整UI地址为:pins_page.dart
编写沸点cell
沸点的cell其中有一个难点是content中加载这url,然后url需要翻译成图片,如下:
而从我们获取到的字段来看,这就是单纯的url,所以这里我们需要正则提取相关字段,然后进行拼接。
List<Widget> _buildContent(String content) {
List<Widget> contentList = new List();
RegExp url = new RegExp(r"((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+");
List listString = content.split(url);
List listUrl = new List();
Iterable<Match> matches = url.allMatches(content);
int urlIndex = 0;
for (Match m in matches) {
listUrl.add(m.group(0));
}
for (var i = 0; i < listString.length; i++) {
if (listString[i] == '') {
// 空字符串说明应该填充Url
contentList.add(PinsCellLink(
linkUrl: listUrl[urlIndex],
));
urlIndex += 1;
} else {
contentList.add(Text(
listString[i],
style: _textStyle,
overflow: TextOverflow.ellipsis,
maxLines: 5,
));
}
}
return contentList;
}
- 首先我们new一个匹配url的正则
RegExp(r"((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+")
- 将content的数据按照url去分割成数组。将url正则匹配出来的url存到数组中。
- 最后通过遍历来填充之前挖去的字段
这里面我们将文字中的链接抽出来作为一个widget
- lib/widgets/pins_cell_link.dart
class PinsCellLink extends StatelessWidget {
final String linkUrl;
PinsCellLink({Key key, this.linkUrl}) : super(key: key);
@override
Widget build(BuildContext context) {
final Color textColor = Theme.of(context).primaryColor;
return Container(
width: 100.0,
child: InkWell(
onTap: () {
Application.router.navigateTo(context, "/web?url=${Uri.encodeComponent(linkUrl)}&title=${Uri.encodeComponent('掘金沸点')}");
},
child: Row(
children: <Widget>[
Icon(
Icons.link,
color: textColor,
),
Text(
'网页链接',
style: TextStyle(color: textColor),
)
],
),
),
);
}
}
代码地址为:pins_cell_link.dart、pins_list_cell.dart
在cell的content中,这些widget还是需要平铺并且换行展示的。所以这里我们使用 Wrap widget
Widget _renderContent(String content) {
return Wrap(
direction: Axis.horizontal,
verticalDirection: VerticalDirection.down,
spacing: 10.0,
children: _buildContent(content),
);
}
- Wrap widget允许我们设置子widget的排列方式,这里我们设置方向为
Axis.horizontal
横向排列,然后允许我们组件换行。这样就会出现得到我们想要的效果
总结
如上,我们只是完成了沸点列表页面的一部分,限于篇幅和知识点的吸收,我们将沸点cell的剩余代码编写放到下一章节中。下一章节,我们将完成图片的查看、轮播图,设置页面切换动画以及图片和链接的cellUI编写。