Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

沸点 UI & 功能 编写(上) #28

Open
Nealyang opened this issue Mar 19, 2019 · 0 comments
Open

沸点 UI & 功能 编写(上) #28

Nealyang opened this issue Mar 19, 2019 · 0 comments
Labels

Comments

@Nealyang
Copy link
Owner

介绍

这一章节代码量可能会比较大,我们将完成沸点的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编写。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant