这里以知乎日报为例,实现一个小的 Demo 来学习 Flutter 的相关知识,使用的 api 来源于网上,仅供学习交流,若有侵权,请联系我。html
先看一下效果:git
项目结构以下:github
用到的几个相关的 api 都在 config 中定义:编程
class Config {
/// Config 中定义常量
static const DEBUG = true;
///最新消息
static const String LAST_NEWS = "https://news-at.zhihu.com/api/4/news/latest";
///热门
static const String HOT_NEWS = "https://news-at.zhihu.com/api/3/news/hot";
///栏目
static const String COLUMN = "https://news-at.zhihu.com/api/3/sections";
static const String COLUMN_DETAIL = "https://news-at.zhihu.com/api/3/section/";
///详情
static const String NEWS_DETAIL = "http://news-at.zhihu.com/api/3/news/";
///历史消息
static const HISTORY_NEWS = "https://news-at.zhihu.com/api/4/news/before/";
}
复制代码
在 main.dart 中实现了 tab 页及切换功能。json
class _MyHomePageState extends State<MyHomePage> {
List<String> titleList = new List();
int _index = 0;
String title = "";
List<Widget> list = new List();
@override
void initState() {
super.initState();
list..add(HomePageMain())..add(HotNewsMain())..add(ColumnPageMain());
titleList..add("首页")..add("热门")..add("栏目");
title = titleList[_index];
}
void _onItemTapped(int index){
if(mounted){
setState(() {
_index = index;
title = titleList[_index];
});
}
}
@override
Widget build(BuildContext context) {
ScreenUtil.instance = ScreenUtil()..init(context);
return Scaffold(
/*appBar: AppBar(
title: Text(title),
),*/
body: list[_index],
bottomNavigationBar:
new BottomNavigationBar(
type: BottomNavigationBarType.fixed,
iconSize: ScreenUtil().setSp(48),
currentIndex: _index,
onTap: _onItemTapped,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(title: Text("首页"),icon: Icon(Icons.home,size: ScreenUtil.getInstance().setWidth(80),)),
BottomNavigationBarItem(title: Text("热门"),icon: Icon(Icons.bookmark_border,size: ScreenUtil.getInstance().setWidth(80),),),
BottomNavigationBarItem(title: Text("栏目"),icon: Icon(Icons.format_list_bulleted,size: ScreenUtil.getInstance().setWidth(80),)),
]
),
);
}
}
复制代码
tab 页及切换仍是经过 BottomNavigationBar 来实现的。BottomNavigationBarItem 是底部的 item。而三个页面作为 widget 存储到了 list 中。api
list..add(HomePageMain())..add(HotNewsMain())..add(ColumnPageMain());
复制代码
而 body 指定为 list 中的 widget ,在经过底部点击事件里面的 setState 实现页面切换。bash
body: list[_index],
复制代码
以前写过的一篇文章RefreshIndicator+FutureBuilder 实现下拉刷新上滑加载数据 介绍了数据刷新的内容,这里只不过把功能在完善一下 。异步网络请求仍是经过 FutureBuilder 来实现的,下拉刷新经过 RefreshIndicator,里面有 onRefresh 回调方法,那里进行网络请求。微信
body: RefreshIndicator(
onRefresh: getItemNews,
child: new CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
new SliverAppBar(
automaticallyImplyLeading: false,
centerTitle: false,
elevation: 2,
forceElevated: false,
// backgroundColor: Colors.white,
brightness: Brightness.dark,
textTheme: TextTheme(),
primary: true,
titleSpacing: 0,
expandedHeight: ScreenUtil.getInstance().setHeight(600),
floating: true,
pinned: true,
snap: true,
flexibleSpace:
new MyFlexibleSpaceBar(
background: Container(
color: Colors.black,
child: ///异步网络请求布局
FutureBuilder<Map<String,dynamic>>(
future: futureGetLastTopNews,
builder: (context,AsyncSnapshot<Map<String,dynamic>> async){
///正在请求时的视图
if (async.connectionState == ConnectionState.active || async.connectionState == ConnectionState.waiting) {
return Container();
}
///发生错误时的视图
if (async.connectionState == ConnectionState.done) {
if (async.hasError) {
return Container();
} else if (async.hasData && async.data != null && async.data.length > 0) {
Map<String,dynamic> newsMap = async.data;
List<dynamic> stories = newsMap["top_stories"];
return Swiper(
itemBuilder: (c, i) {
return InkWell(
child:
Stack(
children: <Widget>[
Opacity(
opacity: 0.8,
child: Container(
decoration: new BoxDecoration(
image: DecorationImage(image:NetworkImage(stories[i]["image"].toString()),fit: BoxFit.fill),
),
),
),
Positioned(
child: Container(
height: ScreenUtil.getInstance().setHeight(250),
width: ScreenUtil.getInstance().setWidth(1080),
// color:Colors.white,
padding: EdgeInsets.symmetric(horizontal: ScreenUtil.getInstance().setWidth(50)),
child: Text(stories[i]["title"].toString(),
softWrap: true,
style: TextStyle(fontSize: ScreenUtil.getInstance().setSp(65),
color: Colors.white,
//fontWeight: FontWeight.bold
),
),
),
// left: ScreenUtil.getInstance().setWidth(50),
bottom: ScreenUtil.getInstance().setHeight(20),
),
],
),
onTap: (){
String id = stories[i]["id"].toString();
Navigator.push(context,
PageRouteBuilder(
transitionDuration: Duration(microseconds: 100),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return new FadeTransition(
opacity: animation,
child: NewsDetailPage(id:id)
);
})
);
},
);
},
autoplay: true,
duration: 500,
itemCount: stories.length,
pagination: new SwiperPagination(
alignment: Alignment.bottomCenter,
margin: EdgeInsets.only(left: ScreenUtil.getInstance().setWidth(100),bottom: ScreenUtil.getInstance().setWidth(40)),
builder: DotSwiperPaginationBuilder(
size: 7,
activeSize: 7,
color:MyColors.gray_ef,
activeColor: MyColors.gray_cc,
)),
);
}else{
return Container();
}
}
return Container();
},
),
),
title: Text("知乎日报",),
titlePadding: EdgeInsets.only(left: 20,bottom: 20),
),
),
FutureBuilder<List<HomeNewsBean>>(
future: futureGetItemNews,
builder: (context,AsyncSnapshot<List<HomeNewsBean>> async){
///正在请求时的视图
if (async.connectionState == ConnectionState.active || async.connectionState == ConnectionState.waiting) {
return getBlankItem();
}
///发生错误时的视图
if (async.connectionState == ConnectionState.done) {
if (async.hasError) {
return getBlankItem();
} else if (async.hasData && async.data != null && async.data.length > 0) {
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if(index < async.data.length){
return _buildItem(async.data[index]);
}else{
return Center(
child: isShowProgress? CircularProgressIndicator(
strokeWidth: 2.0,
):Container(),
);
}
},
childCount: async.data.length + 1,
),
);
}else{
return getBlankItem();
}
}
return getBlankItem();
},
),
]),
),
复制代码
对于上滑数据加载,经过 ScrollerController 来实现的,主要就是对滑动进行监听,若是是滚动到了最下面,则回调加载数据的函数。网络
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
print("get more");
_getMore(currentDate);
}
});
复制代码
为了更好的用户体验,在加载数据的时候,通常都有一个加载进度的动画,这里用了 CircularProgressIndicator。具体就是指定 FutureBuilder 的数据长度为网络请求的数据长度 + 1,最后一个就是为了显示这个小控件的。代码里面根据 index 来决定返回数据视图仍是加载动画视图app
if(index < async.data.length){
return _buildItem(async.data[index]);
}else{
return Center(
child: isShowProgress? CircularProgressIndicator(
strokeWidth: 2.0,
):Container(),
);
}
复制代码
变量 isShowProgress 控制是否显示加载动画的。
知乎里面返回的详情数据里面是 Html 格式的,这里经过一个插件: flutter_html_view 来实现数据的加载。 仍是经过 FutureBuilder 来请求和展现数据。 折叠工具栏经过 NestedScrollView + SliverAppBar 来实现。
class _NewsDetailPageState extends State<NewsDetailPage>
{
///网络请求
Response response;
Dio dio = new Dio();
Future getNewsDetailFuture;
String title = "";
@override
void initState() {
super.initState();
getNewsDetailFuture = getDetailNews();
}
Future<Map<String,dynamic>> getDetailNews() async{
response = await dio.get(Config.NEWS_DETAIL + widget.id,options: Options(responseType: ResponseType.json));
if(response.data != null && response.data["name"] != null){
title = response.data["name"].toString();
setState(() {
});
}
print("消息详情:" + response.data.toString());
return response.data;
}
@override
Widget build(BuildContext context) {
return new Scaffold(
body: FutureBuilder<Map<String,dynamic>>(
future: getNewsDetailFuture,
builder: (context,AsyncSnapshot<Map<String,dynamic>> async){
///正在请求时的视图
if (async.connectionState == ConnectionState.active || async.connectionState == ConnectionState.waiting) {
return Container();
}
///发生错误时的视图
if (async.connectionState == ConnectionState.done) {
if (async.hasError) {
return Container();
} else if (async.hasData && async.data != null && async.data.length > 0) {
Map<String,dynamic> newsMap = async.data;
// List<dynamic> columnNewList = newsMap["stories"];
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
automaticallyImplyLeading: true,
/* leading: Container(
alignment: Alignment.centerLeft,
child: new IconButton(icon: Icon(
Icons.arrow_back, color: Colors.black,
),
onPressed: () {
Navigator.of(context).pop();
}
)
),
*/
centerTitle: false,
elevation: 0,
forceElevated: false,
// backgroundColor: Colors.white,
brightness: Brightness.dark,
textTheme: TextTheme(),
primary: true,
titleSpacing: 0.0,
expandedHeight: ScreenUtil.getInstance().setHeight(550),
floating: false,
pinned: true,
snap: false,
flexibleSpace:
new FlexibleSpaceBar(
background: Container(
child:Image.network(newsMap["image"].toString(),fit: BoxFit.fitWidth,),
),
title:Text(
newsMap["title"].toString(),
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyle(
color: Colors.white,
fontSize: ScreenUtil.getInstance().setSp(50)
),
),
centerTitle: true,
titlePadding: EdgeInsets.only(left: 80,right: 100,bottom: 18),
collapseMode: CollapseMode.parallax,
),
),
];
},
body:
ScrollConfiguration(
behavior: MyBehavior(),
child: SingleChildScrollView(
child: new HtmlView(
padding: EdgeInsets.symmetric(horizontal: 15),
data: newsMap["body"],
onLaunchFail: (url) { // optional, type Function
print("launch $url failed");
},
scrollable: false, //false to use MarksownBody and true to use Marksown
),
),
),
);
}else{
return Container();
}
}
return Container();
},
),
);
}
}
复制代码
其余的两个页面都是相似的,就再也不介绍了,更详细的代码请参考 github
欢迎关注「Flutter 编程开发」微信公众号 。