在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
被实例化。
至此,如何实现一个相似的 底部 + 首页顶部导航 完结 ~