2

I'm trying to preserve the state of widget pages when switching between widgets using BottomNavigationBar. I've read here that I can do this using IndexedStack, however, that doesn't work in my case for two reasons:

  1. The Scaffold in which the pages are displayed gets rebuilt when switching between pages because for some, but not all, pages the Scaffold should be extended: Scaffold( extendBody: _pageIndex == 1, ...)
  2. The pages should be built for the first time just when the page is opened for the first time and not right from the start

Here's a small example that shows that IndexStack is not working as intended because the Scaffold rebuilds:

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int _pageIndex = 1;
  List<Widget> _pages = [Text("hi"), Counter()];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      extendBody: _pageIndex == 1,
      appBar: AppBar(),
      body: IndexedStack(
        children: _pages,
        index: _pageIndex,
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Goto 0',),
          BottomNavigationBarItem(icon: Icon(Icons.business), label: 'Goto 1',),
        ],
        currentIndex: _pageIndex,
        onTap: (int index) {
          setState(() {
            _pageIndex = index;
          });
          print("idx " + _pageIndex.toString());
        },
      ),
    );
  }
}

Demo showing that the state is not preserved

This is the Counter which can be replaced by any other stateful widget:


class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

//this part is not important, just to show that state is lost
class _CounterState extends State<Counter> {
  int _count = 0;

  @override
  void initState() {
    _count = 0;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: TextButton(
        child: Text("Count: " + _count.toString(), style: TextStyle(fontSize: 20),),
        onPressed: () {
          setState(() {
            _count++;
          });
        },
      ),
    );
  }
}
witi
  • 91
  • 7

1 Answers1

4

First off, great question! The trick is to use KeyedSubtree, and conditionally render pages depending on if they have been visited yet or not.

You could adapt your code this way to achieve your desired behavior:

class Page {
  const Page(this.subtreeKey, {required this.child});

  final GlobalKey subtreeKey;
  final Widget child;
}

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  var _pageIndex = 1;

  final _pages = [
    Page(GlobalKey(), child: Text('Hi')),
    Page(GlobalKey(), child: Counter()),
  ];

  final _builtPages = List<bool>.generate(2, (_) => false);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      extendBody: _pageIndex == 1,
      appBar: AppBar(),
      body: Stack(
        fit: StackFit.expand,
        children: _pages.map(
          (page) {
            return _buildPage(
              _pages.indexOf(page),
              page,
            );
          },
        ).toList(),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Goto 0',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: 'Goto 1',
          ),
        ],
        currentIndex: _pageIndex,
        onTap: (int index) {
          setState(() {
            _pageIndex = index;
          });
          print("idx " + _pageIndex.toString());
        },
      ),
    );
  }

  Widget _buildPage(
    int tabIndex,
    Page page,
  ) {
    final isCurrentlySelected = tabIndex == _pageIndex;

    _builtPages[tabIndex] = isCurrentlySelected || _builtPages[tabIndex];

    final Widget view = KeyedSubtree(
      key: page.subtreeKey,
      child: _builtPages[tabIndex] ? page.child : Container(),
    );

    if (tabIndex == _pageIndex) {
      return view;
    } else {
      return Offstage(child: view);
    }
  }
}

You should be able to modify this code to add more tabs, functionality, etc.

Alex Hartford
  • 5,110
  • 2
  • 19
  • 36
  • Thanks, Alex, this is helpful! I haven't looked into keys before and that just might do the trick for me! – witi Apr 22 '21 at 11:11
  • @witi Let me know if it works for you! If it does, do accept the answer for future readers. Otherwise, I'd be happy to continue working on it with you! – Alex Hartford Apr 22 '21 at 13:28