这是系列文章的第三篇,为了更快上车,建议按照顺序,将前两篇文章通读一遍。
从原生开发到Flutter教程(一)认识Flutter
从原生开发到Flutter教程(二)新闻列表布局
前面我们已经了解了Flutter
的基本知识,比如Widget
概念、布局理念、UI构建思想等,这篇文章,我们来一起搭建一下UI框架,完善一下首页,从网络获取真实数据渲染列表。
搭建UI基础框架
以iOS
为例,一款应用的UI基础框架,通常以TabBarController+NavigationController
结合实现的。
TabBarController
即Tab
控制器,管理着应用的Tab
模块,如微信的微信、通讯录、发现、我
一共4个Tab
模块。在iOS中,实现TabBarController
的思想逻辑非常简单,他的viewControllers
即模块控制器集合,tabBar
即展示在应用底部的Tab
栏视图。
而NavigationController
即导航控制器,一般进入二级+页面有两种方式,一种是导航push
,另一种是modal present
,前者更常用,覆盖95%+场景。它的实现思想就是压栈弹栈,即FILO
。
说回Flutter
,基础UI构建框架的思想基本等同于上述,但是具体的实现方式,有着不小的差别。
TabBar
上文我们也已经说过,在写
Flutter
的时候,如果不加留意,就会容易出现代码冗长、可读性差、深陷各种括号难以自拔等等问题,所以,我们要时刻注意抽象封装、代码构建组装方式,不能因为所谓的写Flutter
就像是堆积木我们就真的将代码写的也跟堆积木一样。如果不注重代码质量,真的是无脑编码一时爽,后期维护火葬场
。
1、简单分析
如上图,典型的TabBar
布局,Flutter
的TabBar
构建方式跟原生不太一样,毕竟声明式编程与命令式编程还是有着本质的区别。
先创建文件夹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
是放置在Scaffold
的bottomNavigationBar
里面而不是写在AppBar
的bottom
里面。 - 另个就是
State
类要with SingleTickerProviderStateMixin
。
混入了SingleTickerProviderStateMixin
类来实现动画渐变效果。我们知道,动画效果为了细腻真实,控件需要跟屏幕的刷新帧率(FPS)保持同步更新才可以达到好的效果。而我们的Tab组件需要滚动渐变等动画效果,所以Mixin
了SingleTicker
。
这里就谈到了Mixin
的概念,Mixin
的出现是为了解决一个编程语言的难题的,就是多重继承的问题。由于多重继承的特性虽然灵活强大,但是由于其具有结构复杂化、优先顺序模糊、功能冲突等问题,使得面向对象的世界更加错综复杂,所以为能够利用多继承的优点又解决多继承的问题,提出了规格继承和实现继承这两样东西。比如Java中的interface
即规格继承,只声明,不实现。而Mixin
即实现继承,不光继承了方法名还允许有方法的实现。
某种程度上,继承强调I am
,而Mixin
则强调I can
。
上面一段补充了下
Dart
的Mixin
相关概念,由于本教程没有单独讲解Dart
语法的章节,所以Dart
相关知识会穿插在各个小模块讲解,这样可以直接学以致用,利于记忆。
写完了代码,我们怀着无比激动的心情,运行一下,看看是不是我们想要的效果。
flutter run
后,WTF!!! 怎么长这个样子?
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
,将Scaffold
的bottomNavigationBar
属性直接修改成下面即可完成自定义组件嵌入。
...
bottomNavigationBar: ZKTabBar(
tabController: _tabController,
tabs: _tabs,
),
...
再次运行,效果完美。即此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/await
和JavaScript
中的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.dart
和HomeNewsCell.dart
代码使用网络数据
我们在initState
方法中,请求网络数据,并且数据收到后通过setState
,Flutter
会自动更新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,先就酱。