這裏以知乎日報爲例,實現一個小的 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 編程開發」微信公衆號 。