Flutter填坑-如何保持底部导航栏页中子页面状态

前言

我司最近在用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要求子页面可以滑动,可以使用第三种方案。

原文链接
作者
@ke ji

本文版权属于再惠研发团队,欢迎转载,转载请保留出处。

    原文作者:ke ji
    原文地址: https://zhuanlan.zhihu.com/p/45256552
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞