Flutter练习(三)— 仿掘金渲染掘金文章列表

1,314 阅读4分钟

前言

上一章,我实现了moke json数据展示首页的分类,今天自己又写了一个文章列表组件来渲染从掘金上的文章,由于时间较短,样式调整的还是有些问题,后面有时间会进行重构,该项目已经放到github中,欢迎查看

主要步骤如下

  • 构建单个文章的小卡片
  • 文章列表组件
  • 首页展示

单个文章的小卡片组件

在这里我主要将页面数据展示了上去,样式构造还是比较粗糙的

下面主要用到了FlexExpanded 实现了我熟悉的Flex布局

Flex

Flex组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,使用RowColumn会方便一些,因为RowColumn都继承自Flex,参数基本相同,所以能使用Flex的地方基本上都可以使用RowColumnFlex本身功能是很强大的,它也可以和Expanded组件配合实现弹性布局。接下来我们只讨论Flex和弹性布局相关的属性。

Expanded

可以按比例“扩伸” RowColumnFlex子组件所占用的空间。

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application/config/theme.dart';

class ArticleSingle extends StatelessWidget {
  final ArticleModel articleModel;

  ArticleSingle(this.articleModel);

  @override
  Widget build(BuildContext context) {
    List<Widget> getTagList() {
      List<Widget> tagList = [];
      articleModel.tags.forEach((element) {
        tagList.add(RawChip(
          label: Text(element),
          labelStyle: TextStyle(fontSize: 10.0),
          padding: EdgeInsets.all(0),
        ));
      });
      return tagList;
    }

    return Container(
      padding: EdgeInsets.fromLTRB(0, 12, 0, 12),
      alignment: Alignment.centerLeft,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          new Text(
            articleModel.title,
            style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0),
          ),
          Flex(
            direction: Axis.horizontal,
            children: [
              Expanded(
                  flex: 2,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          new Text(articleModel.author,
                              style: TextStyle(
                                  fontWeight: FontWeight.w300, fontSize: 12.0)),
                          SizedBox(width: 10),
                          ...getTagList()
                        ],
                      ),
                      new Text(
                        articleModel.content,
                        style: TextStyle(
                          fontSize: 14.0,
                        ),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                      // 数量
                      Row(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: [
                          Icon(CupertinoIcons.eye,size: 14,color: secondColor,),
                          SizedBox(width: 5),
                          Text(articleModel.viewCount.toString()),
                          SizedBox(width: 10),
                          Icon(Icons.thumb_up,size: 14,color: secondColor,),
                          SizedBox(width: 5),
                          Text(articleModel.diggCount.toString()),
                          SizedBox(width: 10),
                          Icon(Icons.comment,size: 14,color: secondColor,),
                          SizedBox(width: 5),
                          Text(articleModel.commentCount.toString())
                        ],
                      )
                    ],
                  )),
              Expanded(
                  flex: 1,
                  child: Column(
                    children: [
                      Image.network(
                        articleModel.imgUrl,
                        width: 80,
                        height: 80,
                      ),
                    ],
                  ))
            ],
          )
        ],
      ),
    );
  }
}

class ArticleModel {
  final String title; // 文章标题
  final String author; // 作者
  final String content; // 文章副标题
  final String imgUrl; // 渲染的图片
  final int viewCount; // 查看文章的数量
  final int diggCount; // 点赞数量
  final int commentCount; // 评论数量
  final List<String> tags; // 文章标签
  final double ctime;

  ArticleModel(
      {this.title = '',
      this.author = '',
      this.content = '',
      this.imgUrl = '',
      this.viewCount = 0,
      this.diggCount = 0,
      this.commentCount = 0,
      this.tags = const [],
      this.ctime = 0});
}

文章列表组件

ListView

ListView 实现可滚动的组件,其主要参数如下:

  • itemExtent:该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。在ListView中,指定itemExtent比让子组件自己决定自身长度会更高效,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。
  • shrinkWrap:该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView的会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true
  • addAutomaticKeepAlives:该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive 组件中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时它也不会被GC(垃圾回收),它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false
  • addRepaintBoundaries:该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。当可滚动组件滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。 代码如下:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'article_single.dart';

// 文章组件
class ArticleList extends StatefulWidget {
  final List<ArticleModel> articleList;

  ArticleList({this.articleList = const []});

  _ArticleState createState() => _ArticleState();
}

class _ArticleState extends State<ArticleList> {
  ScrollController _controller = new ScrollController(); //ListView控制器

  List<Widget> buildArticle() {
    List<Widget> articles = [];
    widget.articleList.forEach((element) {
      articles.add(new ArticleSingle(element));
    });
    return articles;
  }

  @override
  Widget build(BuildContext context) {
    return new Container(
        child: widget.articleList.length == 0
            ? new Center(
                child: Text('No Data'),
              )
            : ListView(
          controller: _controller,
          children: buildArticle(),
        )
        );
  }
}

首页

在这里,主要是重新封装了下需要的json数据,并将页面渲染

代码如下

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_application/config/theme.dart';
import 'package:flutter_application/pages/common/search/common_search.dart';
import 'package:flutter_application/pages/components/article_list.dart';
import 'package:flutter_application/pages/components/article_single.dart';

class IndexPage extends StatefulWidget {
  _IndexState createState() => _IndexState();
}

class _IndexState extends State<IndexPage> with TickerProviderStateMixin {
  late TabController _tabController;
  var _tabList = [];
  var _articleList = [];

  List<ArticleModel> _articleModels = [];

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    // List<ArticleModel> _articleModels = [];
    // 分类
    rootBundle
        .loadString("assets/json/categories.json")
        .then((value) => {_tabList = json.decode(value)});
    _tabController = TabController(
        length: _tabList.length,
        vsync: this); // categories 的长度为8 ,通常获取类目是走接口,我这边直接moke了json数据
    _tabController.addListener(() {
      switch (_tabController.index) {
        //  TODO 接入 tabBarView
      }
    });

    rootBundle
        .loadString("assets/json/articles.json")
        .then((value) => {_articleList = json.decode(value)});
    _articleModels = [];

    _articleList.forEach((element) {
      var articleInfo = element['article_info'];
      List<String> tags = [];
      var curTags = [];
      curTags = element['tags'];
      curTags.forEach((e) {
        tags.add(e['tag_name']);
      });

      var articleModel = new ArticleModel(
          title: articleInfo['title'],
          author: element['author_user_info']['user_name'],
          content: articleInfo['brief_content'],
          imgUrl: articleInfo['cover_image'],
          viewCount: articleInfo['view_count'],
          diggCount: articleInfo['digg_count'],
          commentCount: articleInfo['comment_count'],
          ctime: double.parse(articleInfo['ctime']),
          tags: tags);
      _articleModels.add(articleModel);
    });

    return new Container(
      // color: Color(0xfff5f6f7), // 背景色
      padding: EdgeInsets.fromLTRB(12, 6, 12, 0),
      child: new Column(
        children: [
          CommonSearch(
            hint: '搜索掘金',
            icon: Icon(CupertinoIcons.search),
            onTap: () => {
              // TODO 接入搜索
            },
          ),
          Expanded(
              child: Scaffold(
            appBar: TabBar(
                controller: _tabController,
                isScrollable: true,
                // 可滚动
                labelColor: primaryColor,
                unselectedLabelColor: firstColor,
                labelStyle: TextStyle(fontSize: 12.0),
                tabs: _tabList
                    .map((e) => Tab(
                          text: e['category_name'],
                        ))
                    .toList()),
            body: new TabBarView(
              controller: _tabController,
              children: _tabList.map((e) {
                return Container(
                    alignment: Alignment.center,
                    child: ArticleList(
                      articleList: _articleModels,
                    ));
              }).toList(),
            ),
          )),
        ],
      ),
    );
  }
}

效果图如下

image.png