大家好,經(jīng)過幾個月的潛水,F(xiàn)lutter出乎意料的火熱,抱歉一直沒有更新,由于加入了創(chuàng)業(yè)團隊,經(jīng)歷了幾波大起大落,現(xiàn)在終于騰出時間搞搞技術,現(xiàn)在和成都的幾位技術極客合作推出門路網(wǎng),正在用flutter實踐開發(fā)APP,也算是對flutter商業(yè)化的小試牛刀,本篇將門路網(wǎng)APP用到的flutter技術進行簡單分享。
之前的新聞APP的實踐項目中,用到了Tab+TabBarView+Tabcontroller的用法,實現(xiàn)了基于scaffold下頂部標簽頁的頁面切換,但是大家都會遇到來回切換頁面導致TabBarView自動重繪的問題,頁面無法停留到切換前的狀態(tài),這個問題也是困擾了我很久,用PageStorageKey搭配Stack+Offstage解決這個問題。
首先,我們自己寫一個TabBar玩玩,為什么呢?因為這樣可以實現(xiàn)控件的高度自定義,順便學一學新的組件用法:
class NewsTab {
String text;
String tab;
NewsTab(this.text,this.tab);
}
//定義tab頁基本數(shù)據(jù)結構
final List<NewsTab> NewsTabs = <NewsTab>[
new NewsTab('金融','financial'),
new NewsTab('科技','technology'),
new NewsTab('醫(yī)療','medical'),
];
class TabNavigation extends StatelessWidget {
TabNavigation({this.currentTab, this.onSelectTab});
final NewsTab currentTab;
final ValueChanged<NewsTab> onSelectTab; //這個參數(shù)比較關鍵,仔細理解下,省了setState()調(diào)用的環(huán)節(jié)
@override
Widget build(BuildContext context) {
return Row(
children: NewsTabs.map((item){
return GestureDetector( //手勢監(jiān)聽控件,用于監(jiān)聽各種手勢
child: Container(
padding: EdgeInsets.fromLTRB(24.0, 0.0, 24.0, 0.0),
child: Text(item.text,style: TextStyle(color: _colorTabMatching(item: item)),),
),
onTap: ()=>onSelectTab(item,)
//onSelectTab函數(shù)的使用非常巧妙,
//相當于定義了一個接口,可操控當前控件以外的數(shù)據(jù)
);
}).toList()
);
}
//定義tab被選中和沒被選中的顏色樣式
Color _colorTabMatching({NewsTab item}) {
return currentTab == item ? Colors.black : Colors.grey;
}
}
為什么要這么做呢?因為我們可以通過onSelectTab函數(shù)對外部數(shù)據(jù)進行控制,主頁面調(diào)用TabNavigation:
class _MainListState extends State<MainList> {
NewsTab _currenttab = NewsTabs[0]; //定義默認打開的Tab頁
void _selectTab(NewsTab tab){ //修改狀態(tài)值
setState(() {
_currenttab = tab;
});
}
TabNavigation(
currentTab: _currenttab,
onSelectTab: _selectTab,
),
....
}
當使用TabNavigation時,向其傳入定義好的_selectTab函數(shù),即可完成狀態(tài)值修改的任務,這也是子控件向父控件傳遞參數(shù)的一種方式,特別適用于子控件修改父控件狀態(tài)值時的場景。
以上是Tab標簽和主頁面的定義,接下來看Tab頁的定義:
class NewsList extends StatefulWidget{
@override
NewsList({this.newsType,this.pageKey});
final PageStorageKey<NewsTab> pageKey; //當前控件唯一標識Key
final String newsType;
NewsListState createState() => new NewsListState();
}
class NewsListState extends State<NewsList>{
....
}
注意看控件唯一標識Key的定義,有關PageStorageKey的說明請參考官方閱讀理解,看不懂可以用谷歌翻譯過一遍,這里不做贅述了,關鍵在PageStorageKey<NewsTab>中的<NewsTab>。PageStorageKey是局部Key,在父控件中定義時不要重復即可,所以我用了NewsTab類型,當然小伙伴也可以定義其他不會重復的值作為標識,不過可能會比我這個麻煩一點,想知道為啥,因為在主頁面下是這樣定義和使用Key的:
class MainList extends StatefulWidget {
const MainList({ Key key }) : super(key: key);
@override
_MainListState createState() => new _MainListState();
}
class _MainListState extends State<MainList> {
//定義Key值,類型名即是構造函數(shù),需要傳入匹配類型的參數(shù)
Map<NewsTab, PageStorageKey<NewsTab>> pageKeys = {
NewsTabs[0]: PageStorageKey<NewsTab>(NewsTabs[0]),
NewsTabs[1]: PageStorageKey<NewsTab>(NewsTabs[1]),
NewsTabs[2]: PageStorageKey<NewsTab>(NewsTabs[2]),
};
...
Widget build(BuildContext context){
return Scaffold(
...
body: Stack( //Stack在初始化時,會將子控件全部渲染,而TabBarView則僅渲染默認子控件
children: NewsTabs.map((item) {
return Offstage( //使用Offstage,把不需要顯示的子控件隱藏起來
offstage: _currenttab != item,
child: NewsList(
pageKey: pageKeys[item], //傳入Key值
newsType: item.tab),
);
}).toList(),
)
}
}
}
這里用到了Stack+Offstage的組合,特性在注釋中可了解,由于這兩個控件可以保留子控件的特性,再加上PageStorageKey<NewsTab>標識,即可以保證NewsList在控件樹中的位置保持不變,從而避免了NewsList被切換后重復渲染的問題。
為了方便理解,我把pageKeys的定義和使用分開進行,也就是在列表控件NewsList初始化的時候,即為其分配了一個PageStorageKey<NewsTab>類型的key值,保證它需要重復使用的時候不被flutter認為是新控件,也就不會觸發(fā)重繪了。當然你也可以這么寫:
NewsList(
pageKey: PageStorageKey<NewsTab>(item),
newsType: item.tab),
)
以上兩種方式,不管怎么寫,都會通過遍歷NewsTabs獲取NewsTab,這樣創(chuàng)建PageStorageKey方便不少。
為什么沒有用GlobalKey?因為用不上,一方面GlobalKey比較耗費資源,存在于APP的整個生命周期,如同全局變量,另一方全局不允許重復定義,萬一在別的地方需要重建相同控件,還得費腦子想辦法避開相同的GlobalKey,免得捅出其他簍子。另外補充一點,只有有狀態(tài)控件才能使用GlobalKey,一看GlobalKey的定義你就明白了:GlobalKey<State<StatefulWidget>>,是給StatefulWidget下的State類使用的。
動圖對比一下Tab+TabBarView+Tabcontroller和PageStorageKey+Stack+Offstage:


可以看到,在頁面初始渲染和切換時,兩者的區(qū)別,前者初始化時僅渲染了一個Tab頁,頁面切換時每個Tab頁都會自動dispose掉,并且新頁面要重新initState,而后者則在初始化時即渲染了所有子Tab頁,頁面切換時沒有dispose,而是僅調(diào)用了有狀態(tài)控件的didChangeDependencies事件。
源碼地址請點擊此處,本次分享僅做解決方案上的思考,也許還有更好的方案,歡迎大家分享。
另外再總結幾個小問題
1.當項目打包APK后,再次修改代碼運行,有一定幾率遇到新代碼不生效
解決辦法:
1). 打開flutter下的這個目錄:[你的地址]\flutter\bin
2). 刪除cache文件夾
3). 命令行中輸入:flutter doctor
4). 等待處理結束后,再次flutter run
我以為直接用命令:flutter clean刪除項目目錄下的build文件夾,重新運行一下就可以解決問題,沒想到運行后報錯:

flutter clean會直接刪除整個build文件夾,都不帶放回收站的,然后就悲劇了,整個項目沒法運行,這時候你需要一句命令滿血復活:
flutter create -i objc。
- -i 是表示iOS項目開發(fā)語言,objc和swift兩個選項,其中objc是默認的。
- -a 是表示Android項目開發(fā)語言,java和kotlin兩個選項,其中java是默認的
此處感謝JarvanMo的分享
2.使用Navigator做頁面跳轉(zhuǎn)時,記得在其使用它的父控件構造函數(shù)或函數(shù)中添加BuildContext屬性

BuildContext屬性在flutter中的意義是控件在控件樹中的錨點,也可以理解為索引,當需要跳轉(zhuǎn)頁面時,需要告訴Navigator當前控件的錨點,以便于在新頁面中點擊返回鍵時,可以回退到原來的頁面,英文好的同學可以查看原閱讀理解。實際上Navigator也是基于此錨點創(chuàng)建頁面錨點堆棧,所以當你需要對一個寫的很深的子控件觸發(fā)頁面跳轉(zhuǎn)時,需要把context參數(shù)從頂層父控件一層一層往下傳。
控件函數(shù)中加入BuildContext context參數(shù)的意義是讓控件明白:我是誰,我從哪里來,要到哪里去,比如:
//這里加入了BuildContext context,是為了把獲取到的context傳遞到子控件,以用于Navigator做頁面跳轉(zhuǎn)
_list(BuildContext context, List dataList){
....
return ListView.builder(
// padding: const EdgeInsets.all(16.0),
itemCount: dataList.length,
itemBuilder: (context, i) {
//context參數(shù)相當于當前控件在控件樹中的錨點,
//缺少這個參數(shù)會導致列表中的項目無法通過MaterialPageRoute進入下一個頁面
return _newsRow(dataList[i],context);
}
);
}
//這里又需要定義context,是從上面的_list傳下來的
_newsRow(Map newsInfo,BuildContext context){
return ListTile(
...
onTap: (){
Navigator.of(context).push( //直到被Navigator.of(context)用到
MaterialPageRoute(
builder: (BuildContext context) => NewsDetail(
id:newsInfo["id"].toString()
)
)
);
},
);
}
那么控件樹是啥呢?相信大家在寫頁面布局的時候應該感受到了什么叫父子控件,整個flutter項目就是N個父子控件串起來的控件樹。
3. 從網(wǎng)絡獲取的json數(shù)據(jù)內(nèi)包含數(shù)組,無法直接被List.add()或List.addAll()
這個問題需要處理兩個問題:
- 用于保存數(shù)據(jù)的
List對象,必須要進行初始化,否則直接調(diào)用list.add()會報null錯誤:
List<Map> list = new List();
-
獲取到的json數(shù)據(jù)鍵值對有數(shù)據(jù)的情況下,無法直接賦值到定義好的
List<Map> list,需要重新組裝數(shù)據(jù),由于獲取到的json鍵值對中有這樣格式的數(shù)據(jù)://獲取到的json數(shù)據(jù) data:{items:[{'k1':'v1'},{'k2':'v2'},{'k3':'v3'},{'k4':'v4'},...]}
我便直接賦值給了上面定義的list變量:
List<Map> list = new List();
list = request['data']['items'];
結果就悲劇了,一直報這個錯:

所以,從網(wǎng)絡請求獲取到的json數(shù)據(jù)默認是Iterable<Map<dynamic,dynamic>>格式,無法直接賦值給List對象,因此需要做一下處理:
List<Map> a = new List(); //這句new很重要,數(shù)組對象實例化,否則無法運行a.add()
if (request['success']==true){
for(int i=0;i<request['data']['items'].length;i++){
a.add(request['data']['items'][i]); //
}
return a; //此處的意義便是把網(wǎng)絡請求獲取到的數(shù)據(jù)標準化,否則無法直接賦值給dataList
}else return null;
這樣就好多了,當然,如果你做了json序列化,請無視這個問題。
篇幅較長見諒,在此感謝大家的支持,想繼續(xù)了解更多Flutter技巧,請關注Flutter圈子,歡迎大牛向這里投稿布道,也可以加入flutter 中文社區(qū)(官方QQ群:338252156)共同成長,謝謝大家~