从原生开发到Flutter教程(三)UI框架构建及网络请求

这是系列文章的第三篇,为了更快上车,建议按照顺序,将前两篇文章通读一遍。
从原生开发到Flutter教程(一)认识Flutter
从原生开发到Flutter教程(二)新闻列表布局
前面我们已经了解了Flutter的基本知识,比如Widget概念、布局理念、UI构建思想等,这篇文章,我们来一起搭建一下UI框架,完善一下首页,从网络获取真实数据渲染列表。

搭建UI基础框架

iOS为例,一款应用的UI基础框架,通常以TabBarController+NavigationController结合实现的。
TabBarControllerTab控制器,管理着应用的Tab模块,如微信的微信、通讯录、发现、我一共4个Tab模块。在iOS中,实现TabBarController的思想逻辑非常简单,他的viewControllers即模块控制器集合,tabBar即展示在应用底部的Tab栏视图。
NavigationController即导航控制器,一般进入二级+页面有两种方式,一种是导航push,另一种是modal present,前者更常用,覆盖95%+场景。它的实现思想就是压栈弹栈,即FILO
说回Flutter,基础UI构建框架的思想基本等同于上述,但是具体的实现方式,有着不小的差别。

TabBar

上文我们也已经说过,在写Flutter的时候,如果不加留意,就会容易出现代码冗长、可读性差、深陷各种括号难以自拔等等问题,所以,我们要时刻注意抽象封装、代码构建组装方式,不能因为所谓的写Flutter就像是堆积木我们就真的将代码写的也跟堆积木一样。如果不注重代码质量,真的是无脑编码一时爽,后期维护火葬场

1、简单分析
TabBar

如上图,典型的TabBar布局,FlutterTabBar构建方式跟原生不太一样,毕竟声明式编程与命令式编程还是有着本质的区别。
先创建文件夹pages,再创建MainTabPage.dart文件放入该文件夹,如下:

-lib/pages
-lib/pages/MainTabPage.dart

我们就是在MainTabPage.dart文件中添加Tab逻辑,实现整个App的Tab层UI框架。
Flutter中,要想实现Tab,需要使用到TabController、TabBarView、TabBar、Tab等类。
我们先简略了解一下:

  • TabController
    管理TabBar、tabBarView所需状态的管理类,一般在项目中我们会实例化一个,当然也可以用系统默认的DefaultTabController
  • TabBarView
    用于展示其子模块页面,他的children属性就是子页面的容器。
  • TabBar
    展示在App底部的Tab选项视图,在这个类中可以设置颜色、选中效果之类的属性。他有个tabs<Tab>属性,是放置Tab(Tab按钮单元控件)的容器。
  • Tab
    添加到TabBar上的按钮单元控件,他有text、icon、child等属性。
2、使用系统原生TabBar编码

先上代码,后面再分析:

// MainTabPage.dart
import 'package:flutter/material.dart';

class MainTabPage extends StatefulWidget {
  @override
  _MainTabPageState createState() => _MainTabPageState();
}

class _MainTabPageState extends State<MainTabPage>
    with SingleTickerProviderStateMixin {
  TabController _tabController;
  final List<Tab> _tabs = <Tab>[
    Tab(
      text: '首页',
      icon: Icon(Icons.home),
    ),
    Tab(
      text: '消息',
      icon: Icon(Icons.message),
    ),
  ];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(vsync: this, length: _tabs.length);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: TabBar(
        labelColor: Colors.blue,
        unselectedLabelColor: Colors.black87,
        controller: _tabController,
        tabs: _tabs,
      ),
      body: TabBarView(
        controller: _tabController,
        children: _tabs.map((Tab tab) {
          return Center(child: Text(tab.text));
        }).toList(),
      ),
    );
  }
}

上面代码有两个需要注意的点:

  • 一是设置TabBar是放置在ScaffoldbottomNavigationBar里面而不是写在AppBarbottom里面。
  • 另个就是State类要with SingleTickerProviderStateMixin
    混入了SingleTickerProviderStateMixin类来实现动画渐变效果。我们知道,动画效果为了细腻真实,控件需要跟屏幕的刷新帧率(FPS)保持同步更新才可以达到好的效果。而我们的Tab组件需要滚动渐变等动画效果,所以MixinSingleTicker
    这里就谈到了Mixin的概念,Mixin的出现是为了解决一个编程语言的难题的,就是多重继承的问题。由于多重继承的特性虽然灵活强大,但是由于其具有结构复杂化、优先顺序模糊、功能冲突等问题,使得面向对象的世界更加错综复杂,所以为能够利用多继承的优点又解决多继承的问题,提出了规格继承和实现继承这两样东西。比如Java中的interface即规格继承,只声明,不实现。而Mixin即实现继承,不光继承了方法名还允许有方法的实现。
    某种程度上,继承强调I am,而Mixin则强调I can

上面一段补充了下DartMixin相关概念,由于本教程没有单独讲解Dart语法的章节,所以Dart相关知识会穿插在各个小模块讲解,这样可以直接学以致用,利于记忆。

写完了代码,我们怀着无比激动的心情,运行一下,看看是不是我们想要的效果。
flutter run后,WTF!!! 怎么长这个样子?

Tab首次运行

3、自定义TabBar

无论是高度还是样式,都显然不是我们想要的,接下来,我们自定义Tab控件。Flutter自定义控件的思想就是,先打散个组件各自实现 ,再组装在一起。
我们先如下创建文件:

-lib/components
-lib/components/ZKTabBar,dart // 为了避免跟系统控件冲突,以自己的名字作为前缀。

直接上代码:

// ZKTabBar.dart
import 'package:flutter/material.dart';

class ZKTabBar extends StatelessWidget {
  final TabController tabController;
  final List<ZKTab> tabs;

  ZKTabBar({
    @required this.tabController,
    @required this.tabs,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 50.0,
      color: Color(0xfff8f8f8),
      child: Column(
        children: <Widget>[
          Container(
            color: Colors.black12,
            height: 0.5,
          ),
          Container(
            height: 49.5,
            child: TabBar(
              labelColor: Color(0xff4574B3),
              indicatorColor: Colors.transparent,
              unselectedLabelColor: Color(0xff333333),
              indicatorSize: TabBarIndicatorSize.label,
              tabs: tabs,
              controller: tabController,
            ),
          ),
        ],
      ),
    );
  }
}

class ZKTab extends StatelessWidget {
  final String title;
  final IconData icon;
  ZKTab({
    @required this.title,
    this.icon,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(top: 8.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Icon(icon),
          Text(
            this.title,
            style: TextStyle(fontSize: 11.5, height: 0.9, fontWeight: FontWeight.w400),
          ),
        ],
      ),
    );
  }
}

封装好自定义TabBar后,我们在MainTabPage.dart就直接使用就行了。
import,将ScaffoldbottomNavigationBar属性直接修改成下面即可完成自定义组件嵌入。

...
bottomNavigationBar: ZKTabBar(
  tabController: _tabController,
  tabs: _tabs,
),
...

再次运行,效果完美。即此TabBar搭建完毕。

自定义TabBar

新闻列表页网络数据加载

在前面我们为了快速看效果,使用的是本地假数据。这个小节我们加载网络数据渲染页面。
首先,我们新创建一个页面,HomeNewsListPage.dart

-lib/pages/HomeNewsListPage.dart

这个页面就是我们的新闻列表首页,在这个文件里面我们请求网络数据渲染页面。渲染逻辑前面已经讲过了,就是用ListView渲染列表,这里我们重点讲一下网络请求。

1、异步网络请求

我们使用Dart提供的网络框架HttpClient,使用起来如下:

HttpClient network = HttpClient();
  Uri uri = Uri(
      scheme: 'http',
      host: 'api.cportal.cctv.com',
      path: '/api/rest/navListInfo/getHandDataListInfoNew',
      query: 'id=Nav-9Nwml0dIB6wAxgd9EfZA160510&toutuNum=5&version=1&p=5&n=20');
  HttpClientRequest request = await network.getUrl(uri);
  HttpClientResponse response = await request.close();
  var responseBody = await response.transform(utf8.decoder).join();
  Map dataDict = json.decode(responseBody);

上面代码,每一行都比较好理解,值得注意的是,好多处地方都出现了await,我们下面来重点讲一下:
Dart中的async/awaitJavaScript中的async/await功能和用法几乎等同。在处理异步任务时,大部分的编程语言的解决思路就是,提供一个回调函数,一般都会在异步处理完成的时候通过回调函数来告知调用者结果。不难想象,如果代码中有大量异步逻辑,并且出现大量异步任务依赖其它异步任务的结果时,必然会出现Future.then回调中套回调情况。举个例子,验证完信息获取下载链接,下载数据,然后存入数据库。

verify(token).then((fileUrl) {
  downloadFile(fileUrl).then((file) {
    saveToDatabase(file).then((success){
      print('存储数据成功!');
    });  
  });
});

那问题来了,怎么消除这种回调地域呢?
JS/Dart给出的答案就是:async/await组合。
他们可以让异步任务如同同步任务一样处理。直接上代码:

void task() async {
  var fileUrl = await verify(token);
  var file = await downloadFile(fileUrl);
  var result = await saveToDatabase(file);
  print('存储数据成功!');
}

相信你很快就会爱上这套写法。
总结几个关键点:

  • async关键字表示该函数是异步函数,即该函数会放进异步队列中执行,异步队列具有开启线程的能力,所以不会阻塞当前线程。async函数可以返回一个Future对象,当然也可以不返回。外部收到Future对象后可以调用then方法实现链式调用,如下:
verify(token).then((fileUrl){
      return downloadFile(fileUrl);
}).then((file){
    return saveToDatabase(file);
}).then((e){
   //执行接下来的操作 
}).catchError((e){
  //错误处理  
  print(e);
});
  • await后面是一个Future,表示等待该异步任务完成,异步完成后才会往下走;注意:await必须出现在async函数内部。
  • 无论是在JavaScript还是Dart中,async/await都只是一个语法糖,编译器或解释器最终都会将其转化为一个Promise(Future)的调用链。
2、创建模型,解析数据

讲解完了HttpClient的用法,接下来,我们解析网络数据成为自己的模型数据,方便渲染页面和用户交互的时候使用。

  • 先创建一个模型类NewsModel.dart
-lib/models
-lib/models/NewsModel.dart
  • 编写模型类代码
    这里的逻辑跟原生开发的模型类的逻辑一样,无非就是一些模型属性的声明和提供一些JSON转模型的方法。代码如下:
// NewsModel.dart
class NewsModel {
  final String title;
  final DateTime publishDate;
  final String imgUrlString;
  NewsModel({
    this.title,
    this.publishDate,
    this.imgUrlString,
  });
  static NewsModel fromDict(Map<String, dynamic> map) {
    NewsModel model = NewsModel(
      title: map['itemTitle'] ?? '新闻标题解析异常',
      publishDate:
          DateTime.fromMillisecondsSinceEpoch(int.parse(map['pubDate'])),
      imgUrlString: ((map['itemImageNew'] as List).first as Map)['imgUrl'],
    );
    return model;
  }
}

  • 解析网络数据到模型
    创建好模型后,我们回到HomeNewsListPage.dart,来编写请求网络数据和解析模型的代码。

这里为了便于展示,我们直接将请求网络数据和解析的代码写在了...Page.dart文件中,在日常原生开发中可能很多情况都是这么写的,在iOS中即写在了ViewController中,事实上这种写法无可厚非。但是,后期一旦业务代码繁重,Page.dart代码即要负责页面渲染,又要负责数据获取与解析,就会变得越发笨重,造成职责模糊,可读性差,违背单一职责SRP和KISS原则,显然不是最佳实践方案。一般的解决方案是多设计一个层,即Service层,来统一管理这些事情。当然,为了便于展示和理解,我们这里先这么写,后期我们讲到架构层面和数据流管理的知识时再调整代码。

网络请求属于典型的异步任务,所以我们提供一个异步函数来请求数据。

Future<List<NewsModel>> requestData() async {
  HttpClient network = HttpClient();
  Uri uri = Uri(
      scheme: 'http',
      host: 'api.cportal.cctv.com',
      path: '/api/rest/navListInfo/getHandDataListInfoNew',
      query: 'id=Nav-9Nwml0dIB6wAxgd9EfZA160510&toutuNum=5&version=1&p=5&n=20');
  HttpClientRequest request = await network.getUrl(uri);
  HttpClientResponse response = await request.close();
  var responseBody = await response.transform(utf8.decoder).join();
  Map dataDict = json.decode(responseBody);

  List rawDatas = dataDict['itemList'] as List;
  var models = rawDatas.map((map) {
    map = map as Map;
    NewsModel model = NewsModel.fromDict(map);
    return model;
  }).toList();
  return models;
}

上面的代码即完成了数据请求与模型转换,我们来分析一下。
(1)首先,我们使用HttpClient请求数据。写法确实有些笨拙,没关系,我们后面会用第三方框架来替代HttpClient
(2)然后我们拿到HttpClientResponse,这是个抽象类,通过await response.transform(utf8.decoder).join()将其转换成UTF-8字符编码json字符串形式,再通过json.decode()json字符串转换为容器对象,这里是转换为Map
(3)通过itemList键取值并解包为List对象,这就是新闻列表的原始数据数组rawDatas,通过map函数将原始数据转换为咱们前面自定义的模型数组即完成了数据的解析。

  • 改造HomeNewsListPage.dartHomeNewsCell.dart代码使用网络数据
    我们在initState方法中,请求网络数据,并且数据收到后通过setStateFlutter 会自动更新node节点完成渲染。
// HomeNewsListPage.dart
class HomeNewsListPageState extends State<HomeNewsListPage> {
  List<NewsModel> dataSource = [];

  void requestDataAndReload() async {
    var models = await requestData();
    print('zhoukang===>$models');
    setState(() {
      dataSource = models;
    });
  }

  @override
  void initState() {
    super.initState();
    requestDataAndReload();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('新闻列表'),
      ),
      body: ListView.builder(
        itemCount: dataSource.length,
        itemBuilder: (context, index) {
          return HomeNewsCell(
            model: dataSource[index],
          );
        },
      ),
    );
  }
}
// HomeNewsCell.dart
...
Text(
  model.title, // 修改这里,使用模型的title
  style: TextStyle(
    fontSize: 15.0,
    color: Color(0xff111111),
    ),
  maxLines: 3,
  overflow: TextOverflow.ellipsis,
),
...
Container(
  height: 85.0,
  width: 115.0,
  margin: EdgeInsets.only(top: 3),
  decoration: BoxDecoration(
  color: Color(0xffeaeaea),
  borderRadius: BorderRadius.circular(5.0),
  image: DecorationImage(
    // image: AssetImage('images/news_image.jpg'),
    image: NetworkImage(model.imgUrlString), // 修改这里,使用模型中的网络图片
    fit: BoxFit.cover,
    ),
  ),
),
3、效果展示
网络数据展示

总结

简单回顾一下今天学到的内容,首先我们搭建了App的UITab框架,由于系统的TabBar样式不是我们想要的,所以我们自定义了TabBar控件。从自定义控件的实践中我们会更加清晰认识到Flutter的布局思想和组件构建组装思想。
然后,我们学会了网络请求数据,然后刷新页面。由于网络请求属于异步任务,我们顺带学习了Dart语言的异步任务的处理方式,领略了JS/Dart语言中的Promise/Future、await、async的用法。

下篇教程,我们来完善一下新闻首页的布局,添加轮播图、标题联动视图、刷新加载数据等,然后再实现一下新闻详情页。OK,先就酱。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容