前言
产品设计时, 一款APP使用底部导航结构, 是司空见惯的, 闲鱼做为国内第一个全Flutter产品, 也使用的是底部导航结构。平常开发过程中, 开发者只要按照官方文档的要求, 使用底部导航的控件就可以轻松实现一个底部导航的APP。但是, 最近在浏览flutter论坛时, 发现一个关于底部导航切换, 如何保持子页面状态的问题? 笔者带着疑问, 一起来探究下。
问题复现
复现子页面重新创建
main.dart
class IndexStackApp extends StatefulWidget {
const IndexStackApp({Key? key}) : super(key: key);
@override
_IndexStackAppState createState() => _IndexStackAppState();
}
class _IndexStackAppState extends State<IndexStackApp> {
int _currentIndex = 0;
final List<BottomNavigationBarItem> _bottomNavItems = [
BottomNavigationBarItem(
backgroundColor: Colors.blue,
icon: Icon(Icons.home),
title: Text("首页"),
),
BottomNavigationBarItem(
backgroundColor: Colors.green,
icon: Icon(Icons.message),
title: Text("消息"),
),
BottomNavigationBarItem(
backgroundColor: Colors.amber,
icon: Icon(Icons.shopping_cart),
title: Text("购物车"),
),
];
final _pages = [
APage(),
BPage(),
CPage(),
];
@override
Widget build(BuildContext context) {
print('------main, ----build');
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
/// 1、这种方式, 得到的子widget每次都会重新build, 也就是重新绘制
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
items: _bottomNavItems,
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
),
),
);
}
}
a.dart
import 'package:flutter/material.dart';
class APage extends StatefulWidget {
const APage({Key? key}) : super(key: key);
@override
_APageState createState() => _APageState();
}
class _APageState extends State<APage> {
@override
void initState() {
// TODO: implement initState
super.initState();
print('------a, initState');
}
@override
Widget build(BuildContext context) {
print('------a----build');
return Container(
width: double.infinity,
height: double.infinity,
color: Colors.indigoAccent,
child: Center(
child: Text('aaaaaaaaaaaaaaaaaaaaaa'),
)
);
}
@override
void deactivate() {
// TODO: implement deactivate
super.deactivate();
print('------a----deactivate');
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
print('------a----dispose');
}
}
b.dart
import 'package:flutter/material.dart';
class BPage extends StatefulWidget {
const BPage({Key? key}) : super(key: key);
@override
_BPageState createState() => _BPageState();
}
class _BPageState extends State<BPage> {
@override
void initState() {
// TODO: implement initState
super.initState();
print('------b, initState');
}
@override
Widget build(BuildContext context) {
print('------b----build');
return Container(
width: double.infinity,
height: double.infinity,
color: Colors.amber,
child: Center(
child: Text('bbbbbbbbbbbbbbbbbbbbbbbbbbbb'),
)
);
}
@override
void deactivate() {
// TODO: implement deactivate
super.deactivate();
print('------b----deactivate');
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
print('------b----dispose');
}
}
c.dart
import 'package:flutter/material.dart';
class CPage extends StatefulWidget {
const CPage({Key? key}) : super(key: key);
@override
_CPageState createState() => _CPageState();
}
class _CPageState extends State<CPage> {
@override
void initState() {
// TODO: implement initState
super.initState();
print('------c, initState');
}
@override
Widget build(BuildContext context) {
print('------c----build');
return Container(
width: double.infinity,
height: double.infinity,
color: Colors.indigoAccent,
child: Center(
child: Text('ccccccccccccccccccc'),
)
);
}
@override
void deactivate() {
// TODO: implement deactivate
super.deactivate();
print('------c----deactivate');
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
print('------c----dispose');
}
}
上面的实现方式, body: _pages[_currentIndex]。每次根据下标_currentIndex从页面数组_pages取出对应的子页面时, 都会重新创建页面。
我们看下控制台的日志。
从日志可以看出, 程序启动后的顺序。
- 会先在
main.dart中根据下标_currentIndex = 0取出APage APage会创建并执行initState、build方法。- 此时, 点击底部导航按钮
购物车, 程序会先执行APage的deactivate使APage处于失活状态 - 再执行
CPage的initState新建CPage - 然后执行
CPage的build - 执行
APage的dispose, 销毁APage
解决方案
频繁新建子页面, 这样显然是有缺陷的, 因为会耗费性能。
IndexedStack
因为, 笔者用的是IndexedStack来实现底部导航的, 所以, 接下来, 我们一起看下。上面main.dart的build方法可以改成如下实现方式。
class IndexStackApp extends StatefulWidget {
const IndexStackApp({Key? key}) : super(key: key);
@override
_IndexStackAppState createState() => _IndexStackAppState();
}
class _IndexStackAppState extends State<IndexStackApp> {
int _currentIndex = 0;
final List<BottomNavigationBarItem> _bottomNavItems = [
BottomNavigationBarItem(
backgroundColor: Colors.blue,
icon: Icon(Icons.home),
title: Text("首页"),
),
BottomNavigationBarItem(
backgroundColor: Colors.green,
icon: Icon(Icons.message),
title: Text("消息"),
),
BottomNavigationBarItem(
backgroundColor: Colors.amber,
icon: Icon(Icons.shopping_cart),
title: Text("购物车"),
),
];
final _pages = [
APage(),
BPage(),
CPage(),
];
@override
Widget build(BuildContext context) {
print('------main, ----build');
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
/// 2、这种方式, 得到的子widget不会每次重新build
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
items: _bottomNavItems,
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
),
),
);
}
}
使用IndexedStack实现底部导航, 点击购物车, 我们一起看看控制台日志。
main.dart, 程序运行后, 会一次性全部加载APage、BPage、CPage三个子页面- 点击底部导航
购物车, 程序只是执行了main.dart的build方法, 并没有加载三个子页面的任何一个
由此可知, 通过IndexedStack组件, 点击底部导航, 根据下标index, 只展示对应的子页面即可, 继续保持子页面状态。
PageView
如果此时产品有了新的要求, 三个子页面, 可以滑动来回切换。那么, 滑动切换子页面, 子页面会不会出现重新绘制呢?如果重新绘制了, 有没有方案, 使子页面保持子页面状态呢?我们一起来看下。
main.dart的代码更新如下:
class _IndexStackAppState extends State<IndexStackApp> {
int _currentIndex = 0;
final List<BottomNavigationBarItem> _bottomNavItems = [
BottomNavigationBarItem(
backgroundColor: Colors.blue,
icon: Icon(Icons.home),
title: Text("首页"),
),
BottomNavigationBarItem(
backgroundColor: Colors.green,
icon: Icon(Icons.message),
title: Text("消息"),
),
BottomNavigationBarItem(
backgroundColor: Colors.amber,
icon: Icon(Icons.shopping_cart),
title: Text("购物车"),
),
];
final _pages = [
APage(),
BPage(),
CPage(),
];
PageController _pageController = PageController();
@override
Widget build(BuildContext context) {
print('------main, ----build');
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
/// 3、PageView方式, 滑动视图
body: PageView(
controller: _pageController,
children: _pages,
onPageChanged: (index){
setState(() {
_currentIndex = index;
});
},
),
bottomNavigationBar: BottomNavigationBar(
items: _bottomNavItems,
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
),
),
);
}
}
运行程序, 并滑动子页面到BPage, 我们一起来看下控制台日志。
- 程序运行,
main.dart只加载APage - 滑动页面到
BPage, 执行BPage的initState、build - 接着
APage子页面deactivate失去活性, 并dispose被销毁 - 再从子页面
BPage滑动到子页面APage时 APage重新initState、build新建页面- 然后,
BPage子页面deactivate失去活性, 并dispose被销毁
从上面日志看, PageView每一次滑动子页面, 前一个页面就会被销毁, 并重新新建、绘制后一个子页面。循环往复。这并不是我们所期望的。这样显然是有缺陷的。那么, 有没有一种方案, 滑动子页面时, 可以保持子页面的状态呢?答案是有的。
AutomaticKeepAliveClientMixin
官方推荐在子页面的state类里面, 混合继承AutomaticKeepAliveClientMixin类就可以解决滑动页面, 子页面销毁的问题
import 'package:flutter/material.dart';
class APage extends StatefulWidget {
const APage({Key? key}) : super(key: key);
@override
_APageState createState() => _APageState();
}
class _APageState extends State<APage> with AutomaticKeepAliveClientMixin {
@override
void initState() {
// TODO: implement initState
super.initState();
print('------a, initState');
}
@override
Widget build(BuildContext context) {
super.build(context);
print('------a----build');
return Container(
width: double.infinity,
height: double.infinity,
color: Colors.indigoAccent,
child: Center(
child: Text('aaaaaaaaaaaaaaaaaaaaaa'),
)
);
}
@override
void deactivate() {
// TODO: implement deactivate
super.deactivate();
print('------a----deactivate');
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
print('------a----dispose');
}
@override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
}
子页面混合继承AutomaticKeepAliveClientMixin, 并实现bool get wantKeepAlive => true;。
控制台日志如下:
- 程序运行,
main.dart先加载APage - 滑动到
BPage, 会加载BPage - 滑动
CPage, 会加载CPage - 再滑动到
APage, 会来到APage
每一次滑动子页面, 并不会销毁前一个页面。解决了切换底部导航, 保持子页面状态的问题。使用步骤如下:
- State类混合继承 AutomaticKeepAliveClientMixin
- 重写 bool get wantKeepAlive => true;
- build方法中调用super.build(context);
参考资料
官网AutomaticKeepAliveClientMixin<T extends StatefulWidget> mixin
Flutter: PageView/TabBarView等控件保存状态的问题解决方案