這是系列文章的第三篇萌焰,為了更快上車颤霎,建議按照順序猪钮,將前兩篇文章通讀一遍获印。
從原生開發(fā)到Flutter教程(一)認(rèn)識Flutter
從原生開發(fā)到Flutter教程(二)新聞列表布局
前面我們已經(jīng)了解了Flutter
的基本知識述雾,比如Widget
概念、布局理念、UI構(gòu)建思想等玻孟,這篇文章唆缴,我們來一起搭建一下UI框架,完善一下首頁取募,從網(wǎng)絡(luò)獲取真實數(shù)據(jù)渲染列表。
搭建UI基礎(chǔ)框架
以iOS
為例蟆技,一款應(yīng)用的UI基礎(chǔ)框架玩敏,通常以TabBarController+NavigationController
結(jié)合實現(xiàn)的。
TabBarController
即Tab
控制器质礼,管理著應(yīng)用的Tab
模塊旺聚,如微信的微信、通訊錄眶蕉、發(fā)現(xiàn)砰粹、我
一共4個Tab
模塊。在iOS中造挽,實現(xiàn)TabBarController
的思想邏輯非常簡單碱璃,他的viewControllers
即模塊控制器集合,tabBar
即展示在應(yīng)用底部的Tab
欄視圖饭入。
而NavigationController
即導(dǎo)航控制器嵌器,一般進(jìn)入二級+頁面有兩種方式,一種是導(dǎo)航push
谐丢,另一種是modal present
爽航,前者更常用,覆蓋95%+場景乾忱。它的實現(xiàn)思想就是壓棧彈棧讥珍,即FILO
。
說回Flutter
窄瘟,基礎(chǔ)UI構(gòu)建框架的思想基本等同于上述衷佃,但是具體的實現(xiàn)方式,有著不小的差別蹄葱。
TabBar
上文我們也已經(jīng)說過纲酗,在寫
Flutter
的時候,如果不加留意新蟆,就會容易出現(xiàn)代碼冗長觅赊、可讀性差、深陷各種括號難以自拔等等問題琼稻,所以吮螺,我們要時刻注意抽象封裝、代碼構(gòu)建組裝方式,不能因為所謂的寫Flutter
就像是堆積木我們就真的將代碼寫的也跟堆積木一樣鸠补。如果不注重代碼質(zhì)量萝风,真的是無腦編碼一時爽,后期維護(hù)火葬場
紫岩。
1规惰、簡單分析
如上圖,典型的TabBar
布局泉蝌,Flutter
的TabBar
構(gòu)建方式跟原生不太一樣歇万,畢竟聲明式編程與命令式編程還是有著本質(zhì)的區(qū)別。
先創(chuàng)建文件夾pages
勋陪,再創(chuàng)建MainTabPage.dart
文件放入該文件夾贪磺,如下:
-lib/pages
-lib/pages/MainTabPage.dart
我們就是在MainTabPage.dart
文件中添加Tab
邏輯,實現(xiàn)整個App的Tab
層UI框架诅愚。
在Flutter
中寒锚,要想實現(xiàn)Tab
,需要使用到TabController违孝、TabBarView刹前、TabBar、Tab
等類雌桑。
我們先簡略了解一下:
- TabController
管理TabBar腮郊、tabBarView
所需狀態(tài)的管理類,一般在項目中我們會實例化一個筹燕,當(dāng)然也可以用系統(tǒng)默認(rèn)的DefaultTabController
轧飞。 - TabBarView
用于展示其子模塊頁面,他的children
屬性就是子頁面的容器撒踪。 - TabBar
展示在App底部的Tab選項視圖过咬,在這個類中可以設(shè)置顏色、選中效果之類的屬性制妄。他有個tabs<Tab>
屬性掸绞,是放置Tab
(Tab按鈕單元控件)的容器。 - Tab
添加到TabBar
上的按鈕單元控件耕捞,他有text衔掸、icon、child
等屬性俺抽。
2敞映、使用系統(tǒng)原生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(),
),
);
}
}
上面代碼有兩個需要注意的點:
- 一是設(shè)置
TabBar
是放置在Scaffold
的bottomNavigationBar
里面而不是寫在AppBar
的bottom
里面磷斧。 - 另個就是
State
類要with SingleTickerProviderStateMixin
振愿。
混入了SingleTickerProviderStateMixin
類來實現(xiàn)動畫漸變效果捷犹。我們知道,動畫效果為了細(xì)膩真實冕末,控件需要跟屏幕的刷新幀率(FPS)保持同步更新才可以達(dá)到好的效果萍歉。而我們的Tab組件需要滾動漸變等動畫效果,所以Mixin
了SingleTicker
档桃。
這里就談到了Mixin
的概念枪孩,Mixin
的出現(xiàn)是為了解決一個編程語言的難題的,就是多重繼承的問題藻肄。由于多重繼承的特性雖然靈活強(qiáng)大蔑舞,但是由于其具有結(jié)構(gòu)復(fù)雜化、優(yōu)先順序模糊仅炊、功能沖突等問題斗幼,使得面向?qū)ο蟮氖澜绺渝e綜復(fù)雜澎蛛,所以為能夠利用多繼承的優(yōu)點又解決多繼承的問題抚垄,提出了規(guī)格繼承和實現(xiàn)繼承這兩樣?xùn)|西。比如Java中的interface
即規(guī)格繼承谋逻,只聲明呆馁,不實現(xiàn)。而Mixin
即實現(xiàn)繼承毁兆,不光繼承了方法名還允許有方法的實現(xiàn)浙滤。
某種程度上,繼承強(qiáng)調(diào)I am
气堕,而Mixin
則強(qiáng)調(diào)I can
纺腊。
上面一段補充了下
Dart
的Mixin
相關(guān)概念,由于本教程沒有單獨講解Dart
語法的章節(jié)茎芭,所以Dart
相關(guān)知識會穿插在各個小模塊講解揖膜,這樣可以直接學(xué)以致用,利于記憶梅桩。
寫完了代碼壹粟,我們懷著無比激動的心情,運行一下宿百,看看是不是我們想要的效果趁仙。
flutter run
后,WTF!!! 怎么長這個樣子垦页?
3雀费、自定義TabBar
無論是高度還是樣式,都顯然不是我們想要的痊焊,接下來坐儿,我們自定義Tab
控件律胀。Flutter
自定義控件的思想就是,先打散個組件各自實現(xiàn) 貌矿,再組裝在一起炭菌。
我們先如下創(chuàng)建文件:
-lib/components
-lib/components/ZKTabBar,dart // 為了避免跟系統(tǒng)控件沖突,以自己的名字作為前綴逛漫。
直接上代碼:
// 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
搭建完畢菩暗。
新聞列表頁網(wǎng)絡(luò)數(shù)據(jù)加載
在前面我們?yōu)榱丝焖倏葱Ч褂玫氖潜镜丶贁?shù)據(jù)旭蠕。這個小節(jié)我們加載網(wǎng)絡(luò)數(shù)據(jù)渲染頁面停团。
首先,我們新創(chuàng)建一個頁面掏熬,HomeNewsListPage.dart
-lib/pages/HomeNewsListPage.dart
這個頁面就是我們的新聞列表首頁佑稠,在這個文件里面我們請求網(wǎng)絡(luò)數(shù)據(jù)渲染頁面。渲染邏輯前面已經(jīng)講過了旗芬,就是用ListView
渲染列表舌胶,這里我們重點講一下網(wǎng)絡(luò)請求。
1疮丛、異步網(wǎng)絡(luò)請求
我們使用Dart提供的網(wǎng)絡(luò)框架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);
上面代碼,每一行都比較好理解誊薄,值得注意的是履恩,好多處地方都出現(xiàn)了await
,我們下面來重點講一下:
Dart中的async/await
和JavaScript
中的async/await
功能和用法幾乎等同暇屋。在處理異步任務(wù)時似袁,大部分的編程語言的解決思路就是,提供一個回調(diào)函數(shù)咐刨,一般都會在異步處理完成的時候通過回調(diào)函數(shù)來告知調(diào)用者結(jié)果昙衅。不難想象,如果代碼中有大量異步邏輯定鸟,并且出現(xiàn)大量異步任務(wù)依賴其它異步任務(wù)的結(jié)果時而涉,必然會出現(xiàn)Future.then
回調(diào)中套回調(diào)情況。舉個例子联予,驗證完信息獲取下載鏈接啼县,下載數(shù)據(jù)材原,然后存入數(shù)據(jù)庫。
verify(token).then((fileUrl) {
downloadFile(fileUrl).then((file) {
saveToDatabase(file).then((success){
print('存儲數(shù)據(jù)成功季眷!');
});
});
});
那問題來了余蟹,怎么消除這種回調(diào)地域
呢?
JS/Dart
給出的答案就是:async/await
組合子刮。
他們可以讓異步任務(wù)如同同步任務(wù)一樣處理威酒。直接上代碼:
void task() async {
var fileUrl = await verify(token);
var file = await downloadFile(fileUrl);
var result = await saveToDatabase(file);
print('存儲數(shù)據(jù)成功!');
}
相信你很快就會愛上這套寫法挺峡。
總結(jié)幾個關(guān)鍵點:
-
async
關(guān)鍵字表示該函數(shù)是異步函數(shù)葵孤,即該函數(shù)會放進(jìn)異步隊列中執(zhí)行,異步隊列具有開啟線程的能力橱赠,所以不會阻塞當(dāng)前線程尤仍。async
函數(shù)可以返回一個Future
對象,當(dāng)然也可以不返回狭姨。外部收到Future
對象后可以調(diào)用then
方法實現(xiàn)鏈?zhǔn)秸{(diào)用宰啦,如下:
verify(token).then((fileUrl){
return downloadFile(fileUrl);
}).then((file){
return saveToDatabase(file);
}).then((e){
//執(zhí)行接下來的操作
}).catchError((e){
//錯誤處理
print(e);
});
-
await
后面是一個Future
,表示等待該異步任務(wù)完成送挑,異步完成后才會往下走绑莺;注意:await
必須出現(xiàn)在async
函數(shù)內(nèi)部暖眼。 - 無論是在
JavaScript
還是Dart
中惕耕,async/await
都只是一個語法糖,編譯器或解釋器最終都會將其轉(zhuǎn)化為一個Promise(Future)
的調(diào)用鏈诫肠。
2司澎、創(chuàng)建模型,解析數(shù)據(jù)
講解完了HttpClient
的用法栋豫,接下來挤安,我們解析網(wǎng)絡(luò)數(shù)據(jù)成為自己的模型數(shù)據(jù),方便渲染頁面和用戶交互的時候使用丧鸯。
- 先創(chuàng)建一個模型類
NewsModel.dart
蛤铜。
-lib/models
-lib/models/NewsModel.dart
- 編寫模型類代碼
這里的邏輯跟原生開發(fā)的模型類的邏輯一樣,無非就是一些模型屬性的聲明和提供一些JSON
轉(zhuǎn)模型的方法丛肢。代碼如下:
// 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'] ?? '新聞標(biāo)題解析異常',
publishDate:
DateTime.fromMillisecondsSinceEpoch(int.parse(map['pubDate'])),
imgUrlString: ((map['itemImageNew'] as List).first as Map)['imgUrl'],
);
return model;
}
}
- 解析網(wǎng)絡(luò)數(shù)據(jù)到模型
創(chuàng)建好模型后围肥,我們回到HomeNewsListPage.dart
,來編寫請求網(wǎng)絡(luò)數(shù)據(jù)和解析模型的代碼蜂怎。
這里為了便于展示穆刻,我們直接將請求網(wǎng)絡(luò)數(shù)據(jù)和解析的代碼寫在了
...Page.dart
文件中,在日常原生開發(fā)中可能很多情況都是這么寫的杠步,在iOS
中即寫在了ViewController
中氢伟,事實上這種寫法無可厚非榜轿。但是,后期一旦業(yè)務(wù)代碼繁重朵锣,Page.dart
代碼即要負(fù)責(zé)頁面渲染谬盐,又要負(fù)責(zé)數(shù)據(jù)獲取與解析,就會變得越發(fā)笨重诚些,造成職責(zé)模糊设褐,可讀性差,違背單一職責(zé)SRP和KISS原則
泣刹,顯然不是最佳實踐方案助析。一般的解決方案是多設(shè)計一個層,即Service
層椅您,來統(tǒng)一管理這些事情外冀。當(dāng)然,為了便于展示和理解掀泳,我們這里先這么寫雪隧,后期我們講到架構(gòu)層面和數(shù)據(jù)流管理的知識時再調(diào)整代碼。
網(wǎng)絡(luò)請求屬于典型的異步任務(wù)员舵,所以我們提供一個異步函數(shù)來請求數(shù)據(jù)脑沿。
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;
}
上面的代碼即完成了數(shù)據(jù)請求與模型轉(zhuǎn)換,我們來分析一下马僻。
(1)首先庄拇,我們使用HttpClient
請求數(shù)據(jù)。寫法確實有些笨拙韭邓,沒關(guān)系措近,我們后面會用第三方框架來替代HttpClient
。
(2)然后我們拿到HttpClientResponse
女淑,這是個抽象類瞭郑,通過await response.transform(utf8.decoder).join()
將其轉(zhuǎn)換成UTF-8字符編碼
的json
字符串形式,再通過json.decode()
將json
字符串轉(zhuǎn)換為容器對象鸭你,這里是轉(zhuǎn)換為Map
屈张。
(3)通過itemList
鍵取值并解包為List
對象,這就是新聞列表的原始數(shù)據(jù)數(shù)組rawDatas
袱巨,通過map
函數(shù)將原始數(shù)據(jù)轉(zhuǎn)換為咱們前面自定義的模型數(shù)組即完成了數(shù)據(jù)的解析阁谆。
- 改造
HomeNewsListPage.dart
和HomeNewsCell.dart
代碼使用網(wǎng)絡(luò)數(shù)據(jù)
我們在initState
方法中,請求網(wǎng)絡(luò)數(shù)據(jù)瓣窄,并且數(shù)據(jù)收到后通過setState
笛厦,Flutter
會自動更新node
節(jié)點完成渲染。
// 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), // 修改這里裳凸,使用模型中的網(wǎng)絡(luò)圖片
fit: BoxFit.cover,
),
),
),
3贱鄙、效果展示
總結(jié)
簡單回顧一下今天學(xué)到的內(nèi)容,首先我們搭建了App的UITab
框架姨谷,由于系統(tǒng)的TabBar
樣式不是我們想要的逗宁,所以我們自定義了TabBar
控件。從自定義控件的實踐中我們會更加清晰認(rèn)識到Flutter
的布局思想和組件構(gòu)建組裝思想梦湘。
然后瞎颗,我們學(xué)會了網(wǎng)絡(luò)請求數(shù)據(jù),然后刷新頁面捌议。由于網(wǎng)絡(luò)請求屬于異步任務(wù)哼拔,我們順帶學(xué)習(xí)了Dart
語言的異步任務(wù)的處理方式,領(lǐng)略了JS/Dart
語言中的Promise/Future瓣颅、await倦逐、async
的用法。
下篇教程宫补,我們來完善一下新聞首頁的布局檬姥,添加輪播圖、標(biāo)題聯(lián)動視圖粉怕、刷新加載數(shù)據(jù)等健民,然后再實現(xiàn)一下新聞詳情頁。OK贫贝,先就醬恒傻。