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 编程开发」微信公众号 。

相关文章
相关标签/搜索