Flutter 版知乎日報簡單實現

Flutter程式設計開發發表於2019-09-15

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

先看一下效果:

Flutter 版知乎日報簡單實現

一、專案結構以及用到的幾個 API

專案結構如下:

Flutter 版知乎日報簡單實現

  • 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 程式設計開發」微信公眾號 。

Flutter 版知乎日報簡單實現

相關文章