前言
我司最近在用flutter重写一个项目(RN弃坑中),因为刚入手新技术栈,踩坑填坑必不可少,此文记录了我们在改写主页面(底部导航栏+子页面)时遇到的一个问题:当点击底部item切换到另一页面, 再返回此页面时会重走它的initState方法(我们一般在initState中发起网络请求),导致不必要的开销。
原因分析
先看一下我们最开始的写法,我们用列表保存了四个页面,在切换页面时直接返回对应的实例
class BottomNavigationScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() => BottomNavigationState();
}
class BottomNavigationState extends State<BottomNavigationScreen> {
TabItem currentItem = TabItem.home;
var screens = <Widget>[];
int index = 0; // 当前选中页面
@override
void initState() {
super.initState();
screens.add(Page1());
screens.add(Page2());
screens.add(Page3());
screens.add(Page4());
}
_updateCurrentItem(int index) {
setState(() {
this.index = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _buildBody(),
bottomNavigationBar: _buildBottomNavigationBar(),
);
}
Widget _buildBody() {
return screens[index];
}
...
}
在子页面的几个回调方法中添加log,发现再次切到子页面都会触发dispose
,createState
,initState
class MarketingScreen extends StatefulWidget {
final Store<ReduxState> store;
MarketingScreen(this.store){
print('MarketingScreen constructor');
}
@override
State<StatefulWidget> createState() {
print('MarketingScreen createState');
return _MarketingScreenState();
}
}
class _MarketingScreenState extends State<MarketingScreen> {
@override
void initState() {
super.initState();
print('_MarketingScreenState initState');
// send api request }
@override
void dispose() {
super.dispose();
print('_MarketingScreenState dispose');
}
@override
Widget build(BuildContext context) {
print('_MarketingScreenState build');
return ...
}
}
查看createState
,initState
的源码可以知道,当视图树先移除一个Widget,再添加时会调用createSstate
,系统会为新创建的state
调用仅且只有一次initState
。理解了这一特性后,我们就明白了切换页面导致数据丢失的原因。
abstract class StatefulWidget extends Widget {
/// Creates the mutable state for this widget at a given location in the tree. /// ... /// The framework can call this method multiple times over the lifetime of /// a [StatefulWidget]. For example, if the widget is inserted into the tree /// in multiple locations, the framework will create a separate [State] object /// for each location. Similarly, if the widget is removed from the tree and /// later inserted into the tree again, the framework will call [createState] /// again to create a fresh [State] object, simplifying the lifecycle of /// [State] objects. @protected
State createState();
}
abstract class State<T extends StatefulWidget> extends Diagnosticable {
/// Called when this object is inserted into the tree. /// /// The framework will call this method exactly once for each [State] object /// it creates. /// ... @protected
@mustCallSuper
void initState() {
assert(_debugLifecycleState == _StateLifecycle.created);
}
}
解决办法
显然我们不是第一个遇到这个问题的人,通过一些搜索我们找到了几种解决办法。
1. Stack + OffStage + TickerMode
- Stack:类似Android里的
FrameLayout
,按照children的顺序依次绘制。用它可以保存所有子页面的状态。 - OffStage:可以控制控件是否参与布局和绘制
- TickerMode:可以控制控件是否动画
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(children: <Widget>[
_buildPage(0),
_buildPage(1),
_buildPage(2),
_buildPage(3),
]),
bottomNavigationBar: _buildBottomNavigationBar(),
);
} // @override
/// You should keep each page by Stack to keep their state. /// Offstage stops painting, TickerMode stops animation. Widget _buildPage(int index) {
return Offstage(
offstage: this.index != index,
child: TickerMode(enabled: this.index == index, child: screens[index]));
}
2. IndexedStack
IndexedStack
仅展示index
所对应的页面,切换页面只需要更新index
即可
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: index,
children: screens,
),
bottomNavigationBar: _buildBottomNavigationBar(),
);
}
3. PageView
如果UI要求子页面可以滑动,那么PageView + BottomNavigationBar是一种解决办法。 用到 PageView + BottomNavigationBar 或者 TabBarView + TabBar 的时候大家会发现当切换到另一页面的时候, 前一个页面就会被销毁。这并不是我们所期望的。好在系统提供了一个AutomaticKeepAliveClientMixin
用于自定义是否保存状态。 – PageView 的children需要继承自 StatefulWidget – PageView 的children对应的 State 需要继承自 AutomaticKeepAliveClientMixin
,并实现wantKeepAlive
。更多可以参考这篇文章
class BottomNavigationState4 extends State<BottomNavigationScreen4> {
PageController _pageController;
...
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
children: screens,
// physics: NeverScrollableScrollPhysics(), //PageView 可以设置禁止滑动 如果 pagview 嵌套 pagview 会因为滑动冲突导致父pageView无法滑动 onPageChanged: (index){
setState(() {
this.index = index;
});
},
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (int index) {
setState(() {
this.index = index;
});
_pageController.animateToPage(index, duration: Duration(seconds: 1), curve: ElasticInOutCurve()); // PageView 跳转到指定页面 },
items: <BottomNavigationBarItem>[
_buildItem(icon: Icons.adjust, index: 0),
_buildItem(icon: Icons.clear_all, index: 1),
_buildItem(icon: Icons.arrow_downward, index: 2),
_buildItem(icon: Icons.settings_input_component, index: 3),
]));
}
}
class _MarketingScreenState extends State<MarketingScreen> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; // 返回true }
总结
以上三种方案从代码简洁性角度来看,推荐第二种;如果UI要求子页面可以滑动,可以使用第三种方案。
本文版权属于再惠研发团队,欢迎转载,转载请保留出处。