Flutter實(shí)現(xiàn)頁面切換后保持原頁面狀態(tài)的3種方法
前言:
在Flutter應(yīng)用中,導(dǎo)航欄切換頁面后默認(rèn)情況下會丟失原頁面狀態(tài),即每次進(jìn)入頁面時(shí)都會重新初始化狀態(tài),如果在initState中打印日志,會發(fā)現(xiàn)每次進(jìn)入時(shí)都會輸出,顯然這樣增加了額外的開銷,并且?guī)砹瞬缓玫挠脩趔w驗(yàn)。
在正文之前,先看一些常見的App導(dǎo)航,以喜馬拉雅FM為例:

它擁有一個(gè)固定的底部導(dǎo)航以及首頁的頂部導(dǎo)航,可以看到不管是點(diǎn)擊底部導(dǎo)航切換頁面還是在首頁左右側(cè)滑切換頁面,之前的頁面狀態(tài)都是始終維持的,下面就具體介紹下如何在flutter中實(shí)現(xiàn)類似喜馬拉雅的導(dǎo)航效果
第一步:實(shí)現(xiàn)固定的底部導(dǎo)航
在通過flutter create生成的項(xiàng)目模板中,我們先簡化一下代碼,將MyHomePage提取到一個(gè)單獨(dú)的home.dart文件,并在Scaffold腳手架中添加bottomNavigationBar底部導(dǎo)航,在body中展示當(dāng)前選中的子頁面。
/// 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]
);
}
}
其中的三個(gè)子頁面結(jié)構(gòu)相同,均顯示一個(gè)計(jì)數(shù)器和一個(gè)加號按鈕,以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),
)
);
}
}
當(dāng)前效果如下:

可以看到,從第二頁切換回第一頁時(shí),第一頁的狀態(tài)已經(jīng)丟失
第二步:實(shí)現(xiàn)底部導(dǎo)航切換時(shí)保持原頁面狀態(tài)
可能有些小伙伴在搜索后會開始直接使用官方推薦的AutomaticKeepAliveClientMixin,通過在子頁面的State類重寫wantKeepAlive為true 。 然而,如果你的代碼和我上面的類似,body中并沒有使用PageView或TabBarView,很不幸的告訴你,踩到坑了,這樣是無效的,原因后面再詳述?,F(xiàn)在我們先來介紹另外兩種方式:
① 使用IndexedStack實(shí)現(xiàn)
IndexedStack繼承自Stack,它的作用是顯示第index個(gè)child,其它c(diǎn)hild在頁面上是不可見的,但所有child的狀態(tài)都被保持,所以這個(gè)Widget可以實(shí)現(xiàn)我們的需求,我們只需要將現(xiàn)在的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實(shí)現(xiàn)
Offstage的作用十分簡單,通過一個(gè)參數(shù)來控制child是否顯示,所以我們同樣可以組合使用Offstage來實(shí)現(xiàn)該需求,其實(shí)現(xiàn)原理與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],
),
],
));
}
}
在上面的兩種方式中都可以實(shí)現(xiàn)保持原頁面狀態(tài)的需求,但這里有一些開銷上的問題,有經(jīng)驗(yàn)的小伙伴應(yīng)該能發(fā)現(xiàn)當(dāng)應(yīng)用第一次加載的時(shí)候,所有子頁狀態(tài)都被實(shí)例化了(>這里的細(xì)節(jié)并不是因?yàn)槲抑苯影炎禹搶?shí)例化放在bodyList里...<),如果在子頁State的initState中打印日志,可以在終端看到一次性輸出了所有子頁的日志。下面就介紹另一種通過繼承AutomaticKeepAliveClientMixin的方式來更好的實(shí)現(xiàn)保持狀態(tài)。
第三步:實(shí)現(xiàn)首頁的頂部導(dǎo)航
首先我們通過配合使用TabBar+TabBarView+AutomaticKeepAliveClientMixin來實(shí)現(xiàn)頂部導(dǎo)航(注意: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(), // 禁止滑動(dòng)
))
],
));
}
}
其中推薦頁、VIP頁、小說頁、直播頁的結(jié)構(gòu)仍和之前的首頁結(jié)構(gòu)相同,僅顯示一個(gè)計(jì)數(shù)器和一個(gè)加號按鈕,以推薦頁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),
));
}
}
保存后測試,

可以看到,現(xiàn)在添加了首頁頂部導(dǎo)航,且默認(rèn)支持左右側(cè)滑,接下來再進(jìn)一步的完善狀態(tài)保持
第四步:實(shí)現(xiàn)首頁頂部導(dǎo)航切換時(shí)保持原頁面狀態(tài)
③ 使用AutomaticKeepAliveClientMixin實(shí)現(xiàn)
寫到這里已經(jīng)很簡單了,我們只需要在首頁導(dǎo)航內(nèi)需要保持頁面狀態(tài)的子頁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),
));
}
}
再次保存測試,

現(xiàn)在已經(jīng)可以看到,不管是切換底部導(dǎo)航還是切換首頁頂部導(dǎo)航,所有的頁面狀態(tài)都可以被保持,并且在應(yīng)用第一次加載時(shí),終端只看到recommend initState的日志,第一次切換首頁頂部導(dǎo)航至vip頁面時(shí),終端輸出vip initState,當(dāng)再次返回推薦頁時(shí),不再輸出recommend initState。
所以,使用TabBarView+AutomaticKeepAliveClientMixin這種方式既實(shí)現(xiàn)了頁面狀態(tài)的保持,又具有類似惰性求值的功能,對于未使用的頁面狀態(tài)不會進(jìn)行實(shí)例化,減小了應(yīng)用初始化時(shí)的開銷。
更新
前面在底部導(dǎo)航介紹了使用IndexedStack和Offstage兩種方式實(shí)現(xiàn)保持頁面狀態(tài),但它們的缺點(diǎn)在于第一次加載時(shí)便實(shí)例化了所有的子頁面State。為了進(jìn)一步優(yōu)化,下面我們使用PageView+AutomaticKeepAliveClientMixin重寫之前的底部導(dǎo)航,其中PageView和TabBarView的實(shí)現(xiàn)原理類似,具體選擇哪一個(gè)并沒有強(qiáng)制要求。更新后的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(), // 禁止滑動(dòng)
));
}
}
然后在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,更新后保存運(yùn)行,應(yīng)用第一次加載時(shí)不會輸出second initState,僅當(dāng)?shù)谝淮吸c(diǎn)擊底部導(dǎo)航切換至該頁時(shí),該子頁的State被實(shí)例化。
至此,如何實(shí)現(xiàn)一個(gè)類似的 底部 + 首頁頂部導(dǎo)航 完結(jié) ~
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對腳本之家的支持。
相關(guān)文章
Android基于自帶的DownloadManager實(shí)現(xiàn)下載功能示例
這篇文章主要介紹了Android基于自帶的DownloadManager實(shí)現(xiàn)下載功能,結(jié)合實(shí)例形式分析了DownloadManager實(shí)現(xiàn)下載功能的具體操作步驟與相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-08-08
Android 嵌套Fragment的使用實(shí)例代碼
本文主要介紹Android Fragment,在這里提供了實(shí)例代碼跟效果圖,希望能幫助有需要的小伙伴2016-07-07
Android ActionBarActivity設(shè)置全屏無標(biāo)題的方法總結(jié)
這篇文章主要介紹了Android ActionBarActivity設(shè)置全屏無標(biāo)題的相關(guān)資料,需要的朋友可以參考下2017-07-07
Android Studio4.0導(dǎo)入OpenCv4.3.0的方法步驟
這篇文章主要介紹了Android Studio4.0導(dǎo)入OpenCv4.3.0的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
基于Android XML解析與保存的實(shí)現(xiàn)
本篇文章小編為大家介紹,基于Android XML解析與保存的實(shí)現(xiàn)。需要的朋友參考下2013-04-04
Flutter輸入框TextField屬性及監(jiān)聽事件介紹
這篇文章主要介紹了Flutter輸入框TextField屬性及監(jiān)聽事件介紹,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2021-11-11
android studio3.3.1代碼提示忽略大小寫的設(shè)置
這篇文章主要介紹了android studio3.3.1代碼提示忽略大小寫的設(shè)置,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
利用adt-bundle輕松搭建Android開發(fā)環(huán)境與Hello world(Windows)
這篇文章主要介紹了利用adt-bundle在Windows下輕松搭建Android開發(fā)環(huán)境與Hello world,感興趣的小伙伴們可以參考一下2016-07-07

