Nealyang/PersonalBlog

沸点 UI & 功能 编写(上)

Nealyang opened this issue · 0 comments

介绍

这一章节代码量可能会比较大,我们将完成沸点的UI以及相应功能编写,完成此篇,你将得到如下的界面效果:

数据准备

由于之前的首页编写中已经介绍了关于本地数据model的使用,这里我们将直接使用线上数据来进行我们的代码编写

通过掘金web版的公开api我们可以知道沸点的请求api地址

  • lib/api.dart
  // 沸点
  static const String PINS_LIST = 'https://short-msg-ms.juejin.im/v1/pinList/recommend';

从沸点的每一个cell中,我们需要去分析构成该UI,大致需要的字段,通常,在我们的项目开发中,这些也是与开发约束的。

通过分析,我们可以看到沸点的每一个cell分为两种,文字+图片 以及文字+链接的形式,当然,其中每一个沸点也可能没有图片,也有的沸点包含主题、文字中含有链接。这些,在我们定义 沸点的数据model的时候都应该包含进去,所以如下,我们提取我们需要字段

  • lib/model/pins_cell.dart
  Map<String, dynamic> user;
  String objectId;
  String uid;
  String content;
  List<String> pictures;
  int commentCount;
  int likedCount;
  String createdAt;
  Map<String, dynamic> topic;
  String url;
  String urlTitle;
  String urlPic;

在数据model中,应该包含他的构造函数以及factory中对请求数据的处理

  factory PinsCell.fromJson(Map<String, dynamic> json) {
      Map<String, dynamic> user = new Map();
      user['avatarLarge'] = json['user']['avatarLarge'];
      user['objectId'] = json['user']['objectId'];
      user['company'] = json['user']['company'];
      user['jobTitle'] = json['user']['jobTitle'];
      user['role'] = json['user']['role'];
      user['userName'] = json['user']['username'];
      user['currentUserFollowed'] = json['user']['currentUserFollowed'];
  
      Map<String, dynamic> topic = new Map();
      // 有的沸点没有topic
      if (json['topic'] != null) {
        topic['objectId'] = json['topic']['objectId'];
        topic['title'] = json['topic']['title'];
      }
  
      List<String> pics = new List();
      // pics = json['pictures'];_TypeError (type 'List<dynamic>' is not a subtype of type 'List<String>')
      json['pictures'].forEach((ele) {
        pics.add(ele);
      });
  
      return PinsCell(
          commentCount: json['commentCount'],
          content: json['content'],
          createdAt: Util.getTimeDuration(json['createdAt']),
          likedCount: json['likedCount'],
          objectId: json['objectId'],
          pictures: pics,
          topic: topic,
          uid: json['uid'],
          url: json['url'],
          urlPic: json['urlPic'],
          urlTitle: json['urlTitle'],
          user: user);
    }
  • 注意上面代码中关于Map和list数据类型的处理,这里我们是不能够直接复制的,否则会出现_TypeError (type 'List<dynamic>' is not a subtype of type 'List<String>')的错误,也就是数据类型转换的问题。 所以对于Map以及List的数据类型,这里我们单独拿出来通过遍历来重新赋值的。
  • 关于沸点的topic字段,有些沸点是不存在的,所以这里我们需要加一层判断,否则在直接取值的时候会报错。当然,这个注意项可能更加的设计到业务一些

定义好数据model后,我们去编写请求方法

  • lib/util/data_util.dart
  // 沸点 列表

  static Future<List<PinsCell>> getPinsListData(
      Map<String, dynamic> params) async {
    List<PinsCell> resultList = new List();
    var response = await NetUtils.get(Api.PINS_LIST, params: params);
    var responseList = response['d']['list'];
    for (int i = 0; i < responseList.length; i++) {
      PinsCell pinsCell;
      try {
        pinsCell = PinsCell.fromJson(responseList[i]);
      } catch (e) {
        print("error $e at $i");
        continue;
      }
      resultList.add(pinsCell);
    }

    return resultList;
  }
  • 数据请求同样适用我们在net_util.dart下封装的get和post方法
  • 拿到数据后根据数据结构获取列表数据封装成我们的Pins model

编写沸点页面UI

沸点页面,我们需要一些变量阿里存储页面信息,比如沸点list、请求参数、翻页等

  • lib/pages/pins_page.dart
  List<PinsCell> _listData = new List();

  Map<String, dynamic> _params = {
    "src": 'web',
    "uid": "",
    "limit": 20,
    "device_id": "",
    "token": ""
  };
  bool _isRequesting = false; //是否正在请求数据的flag
  bool _hasMore = true;
  String before = '';
  ScrollController _scrollController = new ScrollController();

编写相关的请求方法,然后在页面初始化的时候调用,

    void getPinsList(bool isLoadMore) {
      if (_isRequesting || !_hasMore) return;
  
      if (before != '') {
        _params['before'] = before;
      }
      if (!isLoadMore) {
        _params['before'] = '';
      }
      _isRequesting = true;
      before = DateTime.now().toString().replaceFirst(RegExp(r' '), 'T') + 'Z';
      DataUtils.getPinsListData(_params).then((resultData) {
        List<PinsCell> resultList = new List();
        if (isLoadMore) {
          resultList.addAll(_listData);
        }
        resultList.addAll(resultData);
        if (this.mounted) {
          setState(() {
            _listData = resultList;
            _hasMore = resultData.length != 0;
            _isRequesting = false;
          });
        }
      });
    }
  • 当页面正在请求、以及当前页已经是最后一页的时候,不进行请求
  • 里面的before字段是掘金web版请求网络数据翻页的字段,这里跟业务相关,我们可以不去关心
  • 使用我们之前在dataUtil中封装的请求方法,在获取请求数据后,如果是loadMore,则需要将之前list数据叠加,否则为直接赋值。同时需要设置页面的 isRequesting hasMore字段
  • 注意这里我们setState之前判断了页面的mounted,因为在页面退出时我们需要销毁Controller,而请求是异步操作,所以如果我们在页面已经销毁的时候进行setState操作,页面会报错。

    @override
    Widget build(BuildContext context) {
      if (_listData.length > 0) {
        return Container(
          color: Color(0xFFF4F5F5),
          child: ListView.builder(
            itemCount: _listData.length + 1,
            itemBuilder: _itemBuilder,
            controller: _scrollController,
          ),
        );
      } else {
        return Center(
          child: CircularProgressIndicator(),
        );
      }
    }

build方法中比较简单,其实就是初始化一个列表。这里再强调下,使用ListView.builder去实现长列表是非常好的选择,其性能也是非常的优越,会进行一些数据回收工作。

在initState的时候,我们进行一些页面的请求和Controller的初始化工作

  @override
  void initState() {
    getPinsList(false);
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        print('loadMore');
        getPinsList(true);
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

页面完整UI地址为:pins_page.dart

编写沸点cell

沸点的cell其中有一个难点是content中加载这url,然后url需要翻译成图片,如下:

img 而从我们获取到的字段来看,这就是单纯的url,所以这里我们需要正则提取相关字段,然后进行拼接。

    List<Widget> _buildContent(String content) {
      List<Widget> contentList = new List();
      RegExp url = new RegExp(r"((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+");
      List listString = content.split(url);
      List listUrl = new List();
      Iterable<Match> matches = url.allMatches(content);
      int urlIndex = 0;
      for (Match m in matches) {
        listUrl.add(m.group(0));
      }
      for (var i = 0; i < listString.length; i++) {
        if (listString[i] == '') {
          // 空字符串说明应该填充Url
          contentList.add(PinsCellLink(
            linkUrl: listUrl[urlIndex],
          ));
          urlIndex += 1;
        } else {
          contentList.add(Text(
            listString[i],
            style: _textStyle,
            overflow: TextOverflow.ellipsis,
            maxLines: 5,
          ));
        }
      }
      return contentList;
    }
  • 首先我们new一个匹配url的正则RegExp(r"((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+")
  • 将content的数据按照url去分割成数组。将url正则匹配出来的url存到数组中。
  • 最后通过遍历来填充之前挖去的字段

这里面我们将文字中的链接抽出来作为一个widget

  • lib/widgets/pins_cell_link.dart
class PinsCellLink extends StatelessWidget {
  final String linkUrl;

  PinsCellLink({Key key, this.linkUrl}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final Color textColor = Theme.of(context).primaryColor;
    return Container(
      width: 100.0,
      child: InkWell(
        onTap: () {
           Application.router.navigateTo(context, "/web?url=${Uri.encodeComponent(linkUrl)}&title=${Uri.encodeComponent('掘金沸点')}");
        },
        child: Row(
          children: <Widget>[
            Icon(
              Icons.link,
              color: textColor,
            ),
            Text(
              '网页链接',
              style: TextStyle(color: textColor),
            )
          ],
        ),
      ),
    );
  }
}

代码地址为:pins_cell_link.dartpins_list_cell.dart

在cell的content中,这些widget还是需要平铺并且换行展示的。所以这里我们使用 Wrap widget

  Widget _renderContent(String content) {
    return Wrap(
      direction: Axis.horizontal,
      verticalDirection: VerticalDirection.down,
      spacing: 10.0,
      children: _buildContent(content),
    );
  }
  • Wrap widget允许我们设置子widget的排列方式,这里我们设置方向为Axis.horizontal横向排列,然后允许我们组件换行。这样就会出现得到我们想要的效果

img

总结

如上,我们只是完成了沸点列表页面的一部分,限于篇幅和知识点的吸收,我们将沸点cell的剩余代码编写放到下一章节中。下一章节,我们将完成图片的查看、轮播图,设置页面切换动画以及图片和链接的cellUI编写。