flutter實戰6:TAB頁面切換免重繪
大家好,經過幾個月的潛水,Flutter出乎意料的火熱,抱歉一直沒有更新,由於加入了創業團隊,經歷了幾波大起大落,現在終於騰出時間搞搞技術,現在和成都的幾位技術極客合作推出門路網,正在用flutter實踐開發APP,也算是對flutter商業化的小試牛刀,本篇將門路網APP用到的flutter技術進行簡單分享。
之前的新聞APP的實踐專案中,用到了 Tab+TabBarView+Tabcontroller
的用法,實現了基於 scaffold
下頂部標籤頁的頁面切換,但是大家都會遇到來回切換頁面導致 TabBarView
自動重繪的問題,頁面無法停留到切換前的狀態,這個問題也是困擾了我很久,用PageStorageKey搭配 Stack
+ Offstage
解決這個問題。
首先,我們自己寫一個TabBar玩玩,為什麼呢?因為這樣可以實現控制元件的高度自定義,順便學一學新的元件用法:
class NewsTab { String text; String tab; NewsTab(this.text,this.tab); } //定義tab頁基本資料結構 final List<NewsTab> NewsTabs = <NewsTab>[ new NewsTab('金融','financial'), new NewsTab('科技','technology'), new NewsTab('醫療','medical'), ]; class TabNavigation extends StatelessWidget { TabNavigation({this.currentTab, this.onSelectTab}); final NewsTab currentTab; final ValueChanged<NewsTab> onSelectTab;//這個引數比較關鍵,仔細理解下,省了setState()呼叫的環節 @override Widget build(BuildContext context) { return Row( children: NewsTabs.map((item){ return GestureDetector(//手勢監聽控制元件,用於監聽各種手勢 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函式的使用非常巧妙, //相當於定義了一個介面,可操控當前控制元件以外的資料 ); }).toList() ); } //定義tab被選中和沒被選中的顏色樣式 Color _colorTabMatching({NewsTab item}) { return currentTab == item ? Colors.black : Colors.grey; } } 複製程式碼
為什麼要這麼做呢?因為我們可以通過 onSelectTab
函式對外部資料進行控制,主頁面呼叫 TabNavigation
:
class _MainListState extends State<MainList> { NewsTab _currenttab = NewsTabs[0];//定義預設開啟的Tab頁 void _selectTab(NewsTab tab){//修改狀態值 setState(() { _currenttab = tab; }); } TabNavigation( currentTab: _currenttab, onSelectTab: _selectTab, ), .... } 複製程式碼
當使用 TabNavigation
時,向其傳入定義好的 _selectTab
函式,即可完成狀態值修改的任務,這也是子控制元件向父控制元件傳遞引數的一種方式,特別適用於子控制元件修改父控制元件狀態值時的場景。
以上是 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值,型別名即是建構函式,需要傳入匹配型別的引數 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認為是新控制元件,也就不會觸發重繪了。當然你也可以這麼寫:
NewsList( pageKey: PageStorageKey<NewsTab>(item), newsType: item.tab), ) 複製程式碼
以上兩種方式,不管怎麼寫,都會通過遍歷 NewsTabs
獲取 NewsTab
,這樣建立 PageStorageKey
方便不少。
為什麼沒有用 GlobalKey
?因為用不上,一方面 GlobalKey
比較耗費資源,存在於APP的整個生命週期,如同全域性變數,另一方全域性不允許重複定義,萬一在別的地方需要重建相同控制元件,還得費腦子想辦法避開相同的 GlobalKey
,免得捅出其他簍子。另外補充一點,只有有狀態控制元件才能使用 GlobalKey
,一看 GlobalKey
的定義你就明白了: GlobalKey<State<StatefulWidget>>
,是給 StatefulWidget
下的 State
類使用的。
動圖對比一下 Tab+TabBarView+Tabcontroller
和 PageStorageKey+Stack+Offstage
:


可以看到,在頁面初始渲染和切換時,兩者的區別,前者初始化時僅渲染了一個Tab頁,頁面切換時每個Tab頁都會自動 dispose
掉,並且新頁面要重新 initState
,而後者則在初始化時即渲染了所有子Tab頁,頁面切換時沒有 dispose
,而是僅呼叫了有狀態控制元件的 didChangeDependencies
事件。
原始碼地址請 ofollow,noindex">點選此處 ,本次分享僅做解決方案上的思考,也許還有更好的方案,歡迎大家分享。
此處感謝JarvanMo的分享才有了以上的解決方案
另外再總結幾個小問題
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專案開發語言,objc和swift兩個選項,其中objc是預設的。
- -a 是表示Android專案開發語言,java和kotlin兩個選項,其中java是預設的 此處感謝JarvanMo的 分享
2.使用 Navigator
做頁面跳轉時,記得在其使用它的父控制元件建構函式或函式中新增 BuildContext
屬性

BuildContext
屬性在flutter中的意義是控制元件在控制元件樹中的錨點,也可以理解為索引,當需要跳轉頁面時,需要告訴 Navigator
當前控制元件的錨點,以便於在新頁面中點選返回鍵時,可以回退到原來的頁面,英文好的同學可以檢視原閱讀理解。實際上 Navigator
也是基於此錨點建立頁面錨點堆疊,所以當你需要對一個寫的很深的子控制元件觸發頁面跳轉時,需要把 context
引數從頂層父控制元件一層一層往下傳。 控制元件函式中加入 BuildContext context
引數的意義是讓控制元件明白:我是誰,我從哪裡來,要到哪裡去 ,比如:
//這裡加入了BuildContext context,是為了把獲取到的context傳遞到子控制元件,以用於Navigator做頁面跳轉 _list(BuildContext context, List dataList){ .... return ListView.builder( // padding: const EdgeInsets.all(16.0), itemCount: dataList.length, itemBuilder: (context, i) { //context引數相當於當前控制元件在控制元件樹中的錨點, //缺少這個引數會導致列表中的專案無法通過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. 從網路獲取的 json
資料內包含陣列,無法直接被 List.add()
或 List.addAll()
這個問題需要處理兩個問題:
- 用於儲存資料的
List
物件,必須要進行初始化,否則直接呼叫list.add()
會報null
錯誤:
List
-
獲取到的json資料鍵值對有資料的情況下,無法直接賦值到定義好的
List<Map> list
,需要重新組裝資料,由於獲取到的json鍵值對中有這樣格式的資料://獲取到的json資料 data:{items:[{'k1':'v1'},{'k2':'v2'},{'k3':'v3'},{'k4':'v4'},...]} 複製程式碼
我便直接賦值給了上面定義的 list
變數:
List<Map> list = new List(); list = request['data']['items']; 複製程式碼
結果就悲劇了,一直報這個錯:

所以,從網路請求獲取到的 json
資料預設是 Iterable<Map<dynamic,dynamic>>
格式,無法直接賦值給 List
物件,因此需要做一下處理:
List<Map> a = new List();//這句new很重要,陣列物件例項化,否則無法執行a.add() if (request['success']==true){ for(int i=0;i<request['data']['items'].length;i++){ a.add(request['data']['items'][i]);// } return a;//此處的意義便是把網路請求獲取到的資料標準化,否則無法直接賦值給dataList }else return null; 複製程式碼
這樣就好多了,當然,如果你做了json序列化,請無視這個問題。
篇幅較長見諒,在此感謝大家的支援,想繼續瞭解更多Flutter技巧,請關注Flutter圈子,歡迎大牛向這裡投稿佈道,也可以加入flutter 中文社群(官方QQ群:338252156)共同成長,謝謝大家~