前言
上一章,我实现了moke json数据展示首页的分类,今天自己又写了一个文章列表组件来渲染从掘金上的文章,由于时间较短,样式调整的还是有些问题,后面有时间会进行重构,该项目已经放到github中,欢迎查看
主要步骤如下
- 构建单个文章的小卡片
- 文章列表组件
- 首页展示
单个文章的小卡片组件
在这里我主要将页面数据展示了上去,样式构造还是比较粗糙的
下面主要用到了Flex
和Expanded
实现了我熟悉的Flex布局
Flex
Flex
组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,使用Row
或Column
会方便一些,因为Row
和Column
都继承自Flex
,参数基本相同,所以能使用Flex的地方基本上都可以使用Row
或Column
。Flex
本身功能是很强大的,它也可以和Expanded
组件配合实现弹性布局。接下来我们只讨论Flex
和弹性布局相关的属性。
Expanded
可以按比例“扩伸” Row
、Column
和Flex
子组件所占用的空间。
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application/config/theme.dart';
class ArticleSingle extends StatelessWidget {
final ArticleModel articleModel;
ArticleSingle(this.articleModel);
@override
Widget build(BuildContext context) {
List<Widget> getTagList() {
List<Widget> tagList = [];
articleModel.tags.forEach((element) {
tagList.add(RawChip(
label: Text(element),
labelStyle: TextStyle(fontSize: 10.0),
padding: EdgeInsets.all(0),
));
});
return tagList;
}
return Container(
padding: EdgeInsets.fromLTRB(0, 12, 0, 12),
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
new Text(
articleModel.title,
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0),
),
Flex(
direction: Axis.horizontal,
children: [
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
new Text(articleModel.author,
style: TextStyle(
fontWeight: FontWeight.w300, fontSize: 12.0)),
SizedBox(width: 10),
...getTagList()
],
),
new Text(
articleModel.content,
style: TextStyle(
fontSize: 14.0,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
// 数量
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(CupertinoIcons.eye,size: 14,color: secondColor,),
SizedBox(width: 5),
Text(articleModel.viewCount.toString()),
SizedBox(width: 10),
Icon(Icons.thumb_up,size: 14,color: secondColor,),
SizedBox(width: 5),
Text(articleModel.diggCount.toString()),
SizedBox(width: 10),
Icon(Icons.comment,size: 14,color: secondColor,),
SizedBox(width: 5),
Text(articleModel.commentCount.toString())
],
)
],
)),
Expanded(
flex: 1,
child: Column(
children: [
Image.network(
articleModel.imgUrl,
width: 80,
height: 80,
),
],
))
],
)
],
),
);
}
}
class ArticleModel {
final String title; // 文章标题
final String author; // 作者
final String content; // 文章副标题
final String imgUrl; // 渲染的图片
final int viewCount; // 查看文章的数量
final int diggCount; // 点赞数量
final int commentCount; // 评论数量
final List<String> tags; // 文章标签
final double ctime;
ArticleModel(
{this.title = '',
this.author = '',
this.content = '',
this.imgUrl = '',
this.viewCount = 0,
this.diggCount = 0,
this.commentCount = 0,
this.tags = const [],
this.ctime = 0});
}
文章列表组件
ListView
ListView
实现可滚动的组件,其主要参数如下:
itemExtent
:该参数如果不为null
,则会强制children
的“长度”为itemExtent
的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent
代表子组件的高度;如果滚动方向为水平方向,则itemExtent
就代表子组件的宽度。在ListView
中,指定itemExtent
比让子组件自己决定自身长度会更高效,这是因为指定itemExtent
后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。shrinkWrap
:该属性表示是否根据子组件的总长度来设置ListView
的长度,默认值为false
。默认情况下,ListView
的会在滚动方向尽可能多的占用空间。当ListView
在一个无边界(滚动方向上)的容器中时,shrinkWrap
必须为true
。addAutomaticKeepAlives
:该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive
组件中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive
中,在该列表项滑出视口时它也不会被GC(垃圾回收),它会使用KeepAliveNotification
来保存其状态。如果列表项自己维护其KeepAlive
状态,那么此参数必须置为false
。addRepaintBoundaries
:该属性表示是否将列表项(子组件)包裹在RepaintBoundary
组件中。当可滚动组件滚动时,将列表项包裹在RepaintBoundary
中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary
反而会更高效。和addAutomaticKeepAlive
一样,如果列表项自己维护其KeepAlive
状态,那么此参数必须置为false
。 代码如下:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'article_single.dart';
// 文章组件
class ArticleList extends StatefulWidget {
final List<ArticleModel> articleList;
ArticleList({this.articleList = const []});
_ArticleState createState() => _ArticleState();
}
class _ArticleState extends State<ArticleList> {
ScrollController _controller = new ScrollController(); //ListView控制器
List<Widget> buildArticle() {
List<Widget> articles = [];
widget.articleList.forEach((element) {
articles.add(new ArticleSingle(element));
});
return articles;
}
@override
Widget build(BuildContext context) {
return new Container(
child: widget.articleList.length == 0
? new Center(
child: Text('No Data'),
)
: ListView(
controller: _controller,
children: buildArticle(),
)
);
}
}
首页
在这里,主要是重新封装了下需要的json数据,并将页面渲染
代码如下
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_application/config/theme.dart';
import 'package:flutter_application/pages/common/search/common_search.dart';
import 'package:flutter_application/pages/components/article_list.dart';
import 'package:flutter_application/pages/components/article_single.dart';
class IndexPage extends StatefulWidget {
_IndexState createState() => _IndexState();
}
class _IndexState extends State<IndexPage> with TickerProviderStateMixin {
late TabController _tabController;
var _tabList = [];
var _articleList = [];
List<ArticleModel> _articleModels = [];
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
// List<ArticleModel> _articleModels = [];
// 分类
rootBundle
.loadString("assets/json/categories.json")
.then((value) => {_tabList = json.decode(value)});
_tabController = TabController(
length: _tabList.length,
vsync: this); // categories 的长度为8 ,通常获取类目是走接口,我这边直接moke了json数据
_tabController.addListener(() {
switch (_tabController.index) {
// TODO 接入 tabBarView
}
});
rootBundle
.loadString("assets/json/articles.json")
.then((value) => {_articleList = json.decode(value)});
_articleModels = [];
_articleList.forEach((element) {
var articleInfo = element['article_info'];
List<String> tags = [];
var curTags = [];
curTags = element['tags'];
curTags.forEach((e) {
tags.add(e['tag_name']);
});
var articleModel = new ArticleModel(
title: articleInfo['title'],
author: element['author_user_info']['user_name'],
content: articleInfo['brief_content'],
imgUrl: articleInfo['cover_image'],
viewCount: articleInfo['view_count'],
diggCount: articleInfo['digg_count'],
commentCount: articleInfo['comment_count'],
ctime: double.parse(articleInfo['ctime']),
tags: tags);
_articleModels.add(articleModel);
});
return new Container(
// color: Color(0xfff5f6f7), // 背景色
padding: EdgeInsets.fromLTRB(12, 6, 12, 0),
child: new Column(
children: [
CommonSearch(
hint: '搜索掘金',
icon: Icon(CupertinoIcons.search),
onTap: () => {
// TODO 接入搜索
},
),
Expanded(
child: Scaffold(
appBar: TabBar(
controller: _tabController,
isScrollable: true,
// 可滚动
labelColor: primaryColor,
unselectedLabelColor: firstColor,
labelStyle: TextStyle(fontSize: 12.0),
tabs: _tabList
.map((e) => Tab(
text: e['category_name'],
))
.toList()),
body: new TabBarView(
controller: _tabController,
children: _tabList.map((e) {
return Container(
alignment: Alignment.center,
child: ArticleList(
articleList: _articleModels,
));
}).toList(),
),
)),
],
),
);
}
}