Flutter 版知乎日報簡單實現

這裏以知乎日報爲例,實現一個小的 Demo 來學習 Flutter 的相關知識,使用的 api 來源於網上,僅供學習交流,若有侵權,請聯繫我。html

先看一下效果:git

1、項目結構以及用到的幾個 API

項目結構以下:github

  • 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/";

}
複製代碼

2、Tab 頁實現

在 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],
複製代碼

3、主頁的下拉刷新和上滑加載

以前寫過的一篇文章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 控制是否顯示加載動畫的。

4、詳情頁

知乎裏面返回的詳情數據裏面是 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 編程開發」微信公衆號 。

相關文章
相關標籤/搜索