這裡以知乎日報為例,實現一個小的 Demo 來學習 Flutter 的相關知識,使用的 api 來源於網上,僅供學習交流,如有侵權,請聯絡我。
先看一下效果:
一、專案結構以及用到的幾個 API
專案結構如下:
- Column 欄目頁
- Common 公用的資源
- DataBean 主頁的的資料 Bean
- HomePage 主頁
- HotNews 熱門頁
- utils 工具類
- widgets 其他的介面元件
- main.dart 主工程入口
- home_news_detail.dart 詳情頁
用到的幾個相關的 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/";
}
複製程式碼
二、Tab 頁實現
在 main.dart 中實現了 tab 頁及切換功能。
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 中。
list..add(HomePageMain())..add(HotNewsMain())..add(ColumnPageMain());
複製程式碼
而 body 指定為 list 中的 widget ,在通過底部點選事件裡面的 setState 實現頁面切換。
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 來決定返回資料檢視還是載入動畫檢視
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 程式設計開發」微信公眾號 。