Nealyang/PersonalBlog

小册 UI & 功能 编写

Nealyang opened this issue · 0 comments

前言

如上,我们完成了首页、沸点的UI及功能的编写。小册这部分我们没有涉及到的知识点或许就是头部加上横切bar的效果。本篇完成,我们将实现如下效果:

img

TabBar & TabBarView

横切bar的效果,我们使用widget:TabBar + TabBarView。由于我们的顶部导航栏是不确定的(从接口获取数据),所以这里我们需要新建有状态类StatefulWidget,并且需要实例化 TabController 来设置顶部导航栏的个数。

  • lib/pages/book_page.dart
  @override
  Widget build(BuildContext context) {
    if (_navData.length == 0) {
        return Center(
          child: CircularProgressIndicator(),
        );
      }
    return new Scaffold(
      appBar: new AppBar(
        backgroundColor: Theme.of(context).primaryColor,
        title: new TabBar(
          controller: _tabController,
          tabs: _myTabs,
          indicatorColor: Colors.white,
          isScrollable: true,
        ),
      ),
      body: new TabBarView(
        controller: _tabController,
        children: _myTabView,
      ),
    );
  }
  • build 方法比较简单,一如既往的 Scaffold作为类似前端的HTML标签,撑开整个页面架构。
  • 但是这里title我们实例化了TabBar,controller是需要制定长度的一个TabController实例,tab为我们TabBar的子组件。indicatorColor 为tabBar下面当前tab的指示器。
  • TabBarView也跟Tab类似,注意,TabBar里面的tabs和TabBarView里面的children是一一对应关系。

数据准备

老规矩,我们继续去封装我们当前页面所需要的数据model以及数据请求的方法,但是这里,我们需要封装两套,因为tab是一套,tabView里面数据中的cell也需要一套。也就是上面我们说的TabBar和TabBarView需要一一对应的关系。

定义数据model

  • lib/model/book_nav.dart
  class BookNav {
    String name;
    String id;
    String alias;
  
    BookNav({this.alias, this.id, this.name});
  
    factory BookNav.fromJson(Map<String, dynamic> json) {
      return BookNav(alias: json['alias'], name: json['name'], id: json['id']);
    }
  }
  • lib/model/book_cell.dart
  class BookCell {
    String id;
    int sectionCount;
    int buyCount;
    String img;
    String title;
    String userName;
    double price;
  
    BookCell(
        {this.id,
        this.buyCount,
        this.img,
        this.price,
        this.title,
        this.sectionCount,
        this.userName});
  
    factory BookCell.fromJson(Map<String, dynamic> json) {
      return BookCell(
          id: json['_id'],
          buyCount: json['buyCount'],
          img: json['img'],
          title:json['title'],
          price: json['price'],
          sectionCount: json['section'].length,
          userName: json['userData']['username']);
    }
  }

这里就是定义我们界面每一个豆腐块需要的数据

请求数据

lib/api/api.dart 中,定义我们准备好的请求Url

  // 小册导航
  static const String BOOK_NAV = 'https://xiaoce-timeline-api-ms.juejin.im/v1/getNavList';
  static const String BOOK_LIST = 'https://xiaoce-timeline-api-ms.juejin.im/v1/getListByLastTime';
  • lib/util/net_util.dart
  // 获取小册导航栏
  static Future<List<BookNav>> getBookNavData() async {
    List<BookNav> resultList = [];
    var response = await NetUtils.get(Api.BOOK_NAV);
    var responseList = response['d'];
    for (int i = 0; i < responseList.length; i++) {
      BookNav bookNav;
      try {
        bookNav = BookNav.fromJson(responseList[i]);
      } catch (e) {
        print("error $e at $i");
        continue;
      }
      resultList.add(bookNav);
    }

    return resultList;
  }

  // 获取小册
  static Future<List<BookCell>> getBookListData(
      Map<String, dynamic> params) async {
    List<BookCell> resultList = new List();
    var response = await NetUtils.get(Api.BOOK_LIST, params: params);
    var responseList = response['d'];
    for (int i = 0; i < responseList.length; i++) {
      BookCell bookCell;
      try {
        bookCell = BookCell.fromJson(responseList[i]);
      } catch (e) {
        print("error $e at $i");
        continue;
      }
      resultList.add(bookCell);
    }

    return resultList;
  }

写到这里,细心地同学是否发现,我们data_utils.dart里面的代码有很多是重复的?是否可以抽象出来一个模子出来。当然,这里希望大家写完后仔细思考,有好的想法,欢迎在群里交流。

渲染页面

当我们拿到数据后,我们需要准备TabBar还需要准备TabBarView。

  List<Tab> _myTabs = <Tab>[
    Tab(
      text: '全部',
    )
  ];
  List<BookPageTabView> _myTabView = <BookPageTabView>[
    BookPageTabView(
      alias: 'all',
    )
  ];
    TabController _tabController;

initState的时候,请求数据,然后设置以上三个变量的值,去重新渲染页面。

  getNavList() {
    DataUtils.getBookNavData().then((resultData) {
      resultData.forEach((BookNav bn) {
        _myTabs.add(Tab(
          text: bn.name,
        ));
        _myTabView.add(BookPageTabView(
          alias: bn.alias,
        ));
      });
      if (this.mounted) {
        setState(() {
          _navData = resultData;
        });
        _tabController =
            new TabController(vsync: this, length: _navData.length + 1);
      }
    });
  }
  • 注意这里我们同样在setState之前判断了下当前页面是否monted,原因和之前说的一样。

TabBarView页面

如上,我们完成book_page的外壳,下面来编写TabBarView的页面内容

在pages目录下新建 book_page_tab_view.dart注意上面代码,我们在实例化tab_view page的时候是传递alias参数的,这也是我们请求当前tab下全部小册的重要参数。

  Widget _itemBuilder(context,index){
    return BookListCell(cellData: _bookList[index],);
  }

  @override
  Widget build(BuildContext context) {
    if (_bookList.length == 0) {
      return Center(
        child: CircularProgressIndicator(),
      );
    }
    return ListView.builder(
      itemBuilder: _itemBuilder,
      itemCount: _bookList.length,
    );
  }

页面请求逻辑大致和上面相同,我们依然将每一本小册的cell分离出来作为一个组件

  • lib/widget/book_list_cell.dart
@override
  Widget build(BuildContext context) {
    final Color accentColor = Theme.of(context).accentColor;
    final Color primaryColor = Colors.blueAccent;

    return InkWell(
      onTap: () {
        String url = "https://juejin.im/book/${cellData.id}";
        Application.router.navigateTo(context,
            "/web?url=${Uri.encodeComponent(url)}&title=${Uri.encodeComponent(cellData.title)}");
      },
      child: Container(
        padding: EdgeInsets.symmetric(
            horizontal: Util.setPercentage(0.03, context), vertical: 15.0),
        decoration: BoxDecoration(
          color: Colors.white,
          border: Border(
            bottom: BorderSide(color: accentColor, width: 0.5),
          ),
        ),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Container(
              padding: EdgeInsets.only(
                right: Util.setPercentage(0.03, context),
              ),
              child: Image.network(
                cellData.img,
                width: Util.setPercentage(0.2, context),
                height: 100,
                fit: BoxFit.contain,
              ),
            ),
            Container(
              width: Util.setPercentage(0.5, context),
              margin: EdgeInsets.only(
                  right: Util.setPercentage(0.01, context)), //0.8
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text(
                    cellData.title,
                    style: TextStyle(
                      color: Color(0xFF34383B),
                      fontSize: 18.0,
                      fontWeight: FontWeight.bold,
                    ),
                    maxLines: 3,
                    overflow: TextOverflow.ellipsis,
                  ),
                  SizedBox(
                    height: 5.0,
                  ),
                  Text(
                    cellData.userName,
                    style: TextStyle(color: Color(0xFF34383B), fontSize: 16.0),
                  ),
                  SizedBox(
                    height: 5.0,
                  ),
                  Row(
                    children: <Widget>[
                      Text(
                        '${cellData.sectionCount}小节',
                        style: TextStyle(color: accentColor),
                      ),
                      InTextDot(),
                      Text(
                        '${cellData.buyCount}人已购买',
                        style: TextStyle(color: accentColor),
                      )
                    ],
                  )
                ],
              ),
            ),
            Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 15.0, vertical: 5.0),
                decoration: BoxDecoration(
                    color: Color(0xFFF0F7FF),
                    borderRadius: BorderRadius.all(Radius.circular(15.0))),
                child: Text(
                  '¥${cellData.price}',
                  style: TextStyle(color: primaryColor, fontSize: 16.0),
                ))
          ],
        ),
      ),
    );
  }

代码地址:flutter_juejin

总结

如上,我们完成了带有TabBar页面的UI以及功能的编写,后面活动、开源库的UI编写其实也是如此。使用的Widget基本都是我们常用的Widget,代码的编写风格也区域统一。