在Flutter應用中,導航欄切換頁面後默認狀況下會丟失原頁面狀態,即每次進入頁面時都會從新初始化狀態,若是在initState
中打印日誌,會發現每次進入時都會輸出,顯然這樣增長了額外的開銷,而且帶來了很差的用戶體驗。markdown
在正文以前,先看一些常見的App導航,以喜馬拉雅FM爲例:app
它擁有一個固定的底部導航以及首頁的頂部導航,能夠看到無論是點擊底部導航切換頁面仍是在首頁左右側滑切換頁面,以前的頁面狀態都是始終維持的,下面就具體介紹下如何在flutter中實現相似喜馬拉雅的導航效果ide
在經過flutter create
生成的項目模板中,咱們先簡化一下代碼,將MyHomePage
提取到一個單獨的home.dart
文件,並在Scaffold
腳手架中添加bottomNavigationBar
底部導航,在body
中展現當前選中的子頁面。工具
/// home.dart
import 'package:flutter/material.dart';
import './pages/first_page.dart';
import './pages/second_page.dart';
import './pages/third_page.dart';
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final items = [
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('首頁')),
BottomNavigationBarItem(icon: Icon(Icons.music_video), title: Text('聽')),
BottomNavigationBarItem(icon: Icon(Icons.message), title: Text('消息'))
];
final bodyList = [FirstPage(), SecondPage(), ThirdPage()];
int currentIndex = 0;
void onTap(int index) {
setState(() {
currentIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('demo'),
),
bottomNavigationBar: BottomNavigationBar(
items: items,
currentIndex: currentIndex,
onTap: onTap
),
body: bodyList[currentIndex]
);
}
}
複製代碼
其中的三個子頁面結構相同,均顯示一個計數器和一個加號按鈕,以first_page.dart
爲例:測試
/// first_page.dart
import 'package:flutter/material.dart';
class FirstPage extends StatefulWidget {
@override
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
int count = 0;
void add() {
setState(() {
count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('First: $count', style: TextStyle(fontSize: 30))
),
floatingActionButton: FloatingActionButton(
onPressed: add,
child: Icon(Icons.add),
)
);
}
}
複製代碼
當前效果以下:優化
能夠看到,從第二頁切換回第一頁時,第一頁的狀態已經丟失ui
可能有些小夥伴在搜索後會開始直接使用官方推薦的AutomaticKeepAliveClientMixin
,經過在子頁面的State類重寫wantKeepAlive
爲true
。 然而,若是你的代碼和我上面的相似,body中並無使用PageView
或TabBarView
,很不幸的告訴你,踩到坑了,這樣是無效的,緣由後面再詳述。如今咱們先來介紹另外兩種方式:this
① 使用IndexedStack
實現spa
IndexedStack
繼承自Stack
,它的做用是顯示第index
個child
,其它child
在頁面上是不可見的,但全部child
的狀態都被保持,因此這個Widget
能夠實現咱們的需求,咱們只須要將如今的body
用IndexedStack
包裹一層便可日誌
/// home.dart
class _MyHomePageState extends State<MyHomePage> {
...
...
...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('demo'),
),
bottomNavigationBar: BottomNavigationBar(
items: items, currentIndex: currentIndex, onTap: onTap),
// body: bodyList[currentIndex]
body: IndexedStack(
index: currentIndex,
children: bodyList,
));
}
複製代碼
保存後再次測試一下
② 使用Offstage
實現
Offstage
的做用十分簡單,經過一個參數來控制child
是否顯示,因此咱們一樣能夠組合使用Offstage
來實現該需求,其實現原理與IndexedStack
相似
/// home.dart
class _MyHomePageState extends State<MyHomePage> {
...
...
...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('demo'),
),
bottomNavigationBar: BottomNavigationBar(
items: items, currentIndex: currentIndex, onTap: onTap),
// body: bodyList[currentIndex],
body: Stack(
children: [
Offstage(
offstage: currentIndex != 0,
child: bodyList[0],
),
Offstage(
offstage: currentIndex != 1,
child: bodyList[1],
),
Offstage(
offstage: currentIndex != 2,
child: bodyList[2],
),
],
));
}
}
複製代碼
在上面的兩種方式中均可以實現保持原頁面狀態的需求,但這裏有一些開銷上的問題,有經驗的小夥伴應該能發現當應用第一次加載的時候,全部子頁狀態都被實例化了(>這裏的細節並非由於我直接把子頁實例化放在bodyList
裏...<),若是在子頁State
的initState
中打印日誌,能夠在終端看到一次性輸出了全部子頁的日誌。下面就介紹另外一種經過繼承AutomaticKeepAliveClientMixin
的方式來更好的實現保持狀態。
首先咱們經過配合使用TabBar
+TabBarView
+AutomaticKeepAliveClientMixin
來實現頂部導航(注意:TabBar
和TabBarView
須要提供controller
,若是本身沒有定義,則必須使用DefaultTabController
包裹)。此處也能夠選擇使用PageView
,後面會介紹。
咱們先在home.dart
文件移除Scaffold
腳手架中的appBar
頂部工具欄,而後開始重寫首頁first_page.dart
:
/// first_page.dart
import 'package:flutter/material.dart';
import './recommend_page.dart';
import './vip_page.dart';
import './novel_page.dart';
import './live_page.dart';
class _TabData {
final Widget tab;
final Widget body;
_TabData({this.tab, this.body});
}
final _tabDataList = <_TabData>[
_TabData(tab: Text('推薦'), body: RecommendPage()),
_TabData(tab: Text('VIP'), body: VipPage()),
_TabData(tab: Text('小說'), body: NovelPage()),
_TabData(tab: Text('直播'), body: LivePage())
];
class FirstPage extends StatefulWidget {
@override
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
final tabBarList = _tabDataList.map((item) => item.tab).toList();
final tabBarViewList = _tabDataList.map((item) => item.body).toList();
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: tabBarList.length,
child: Column(
children: <Widget>[
Container(
width: double.infinity,
height: 80,
padding: EdgeInsets.fromLTRB(20, 24, 0, 0),
alignment: Alignment.centerLeft,
color: Colors.black,
child: TabBar(
isScrollable: true,
indicatorColor: Colors.red,
indicatorSize: TabBarIndicatorSize.label,
unselectedLabelColor: Colors.white,
unselectedLabelStyle: TextStyle(fontSize: 18),
labelColor: Colors.red,
labelStyle: TextStyle(fontSize: 20),
tabs: tabBarList),
),
Expanded(
child: TabBarView(
children: tabBarViewList,
// physics: NeverScrollableScrollPhysics(), // 禁止滑動
))
],
));
}
}
複製代碼
其中推薦頁、VIP頁、小說頁、直播頁的結構仍和以前的首頁結構相同,僅顯示一個計數器和一個加號按鈕,以推薦頁recommend_page.dart
爲例:
/// recommend_page.dart
import 'package:flutter/material.dart';
class RecommendPage extends StatefulWidget {
@override
_RecommendPageState createState() => _RecommendPageState();
}
class _RecommendPageState extends State<RecommendPage> {
int count = 0;
void add() {
setState(() {
count++;
});
}
@override
void initState() {
super.initState();
print('recommend initState');
}
@override
Widget build(BuildContext context) {
return Scaffold(
body:Center(
child: Text('首頁推薦: $count', style: TextStyle(fontSize: 30))
),
floatingActionButton: FloatingActionButton(
onPressed: add,
child: Icon(Icons.add),
));
}
}
複製代碼
保存後測試,
③ 使用AutomaticKeepAliveClientMixin
實現
寫到這裏已經很簡單了,咱們只須要在首頁導航內須要保持頁面狀態的子頁State
中,繼承AutomaticKeepAliveClientMixin
並重寫wantKeepAlive
爲true
便可。
notes:Subclasses must implement wantKeepAlive, and their build methods must call super.build (the return value will always return null, and should be ignored)
以首頁推薦recommend_page.dart
爲例:
/// recommend_page.dart
import 'package:flutter/material.dart';
class RecommendPage extends StatefulWidget {
@override
_RecommendPageState createState() => _RecommendPageState();
}
class _RecommendPageState extends State<RecommendPage> with AutomaticKeepAliveClientMixin {
int count = 0;
void add() {
setState(() {
count++;
});
}
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
print('recommend initState');
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body:Center(
child: Text('首頁推薦: $count', style: TextStyle(fontSize: 30))
),
floatingActionButton: FloatingActionButton(
onPressed: add,
child: Icon(Icons.add),
));
}
}
複製代碼
再次保存測試,
如今已經能夠看到,無論是切換底部導航仍是切換首頁頂部導航,全部的頁面狀態均可以被保持,而且在應用第一次加載時,終端只看到recommend initState
的日誌,第一次切換首頁頂部導航至vip頁面時,終端輸出vip initState
,當再次返回推薦頁時,再也不輸出recommend initState
。
因此,使用TabBarView
+AutomaticKeepAliveClientMixin
這種方式既實現了頁面狀態的保持,又具備相似惰性求值的功能,對於未使用的頁面狀態不會進行實例化,減少了應用初始化時的開銷。
前面在底部導航介紹了使用IndexedStack
和Offstage
兩種方式實現保持頁面狀態,但它們的缺點在於第一次加載時便實例化了全部的子頁面State
。爲了進一步優化,下面咱們使用PageView
+AutomaticKeepAliveClientMixin
重寫以前的底部導航,其中PageView
和TabBarView
的實現原理相似,具體選擇哪個並無強制要求。更新後的home.dart
文件以下:
/// home.dart
import 'package:flutter/material.dart';
import './pages/first_page.dart';
import './pages/second_page.dart';
import './pages/third_page.dart';
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final items = [
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('首頁')),
BottomNavigationBarItem(icon: Icon(Icons.music_video), title: Text('聽')),
BottomNavigationBarItem(icon: Icon(Icons.message), title: Text('消息'))
];
final bodyList = [FirstPage(), SecondPage(), ThirdPage()];
final pageController = PageController();
int currentIndex = 0;
void onTap(int index) {
pageController.jumpToPage(index);
}
void onPageChanged(int index) {
setState(() {
currentIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
items: items, currentIndex: currentIndex, onTap: onTap),
// body: bodyList[currentIndex],
body: PageView(
controller: pageController,
onPageChanged: onPageChanged,
children: bodyList,
physics: NeverScrollableScrollPhysics(), // 禁止滑動
));
}
}
複製代碼
而後在bodyList
的子頁State
中繼承AutomaticKeepAliveClientMixin
並重寫wantKeepAlive
,以second_page.dart
爲例:
/// second_page.dart
import 'package:flutter/material.dart';
class SecondPage extends StatefulWidget {
@override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> with AutomaticKeepAliveClientMixin {
int count = 0;
void add() {
setState(() {
count++;
});
}
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
print('second initState');
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: Center(
child: Text('Second: $count', style: TextStyle(fontSize: 30))
),
floatingActionButton: FloatingActionButton(
onPressed: add,
child: Icon(Icons.add),
));
}
}
複製代碼
Ok,更新後保存運行,應用第一次加載時不會輸出second initState
,僅當第一次點擊底部導航切換至該頁時,該子頁的State
被實例化。
至此,如何實現一個相似的 底部 + 首頁頂部導航 完結 ~