Preserve state of widget in flutter even though parent widget rebuilds

388

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.

Share:
388
witi
Author by

witi

Updated on December 29, 2022

Comments

  • witi
    witi over 1 year

    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
    witi about 3 years
    Thanks, Alex, this is helpful! I haven't looked into keys before and that just might do the trick for me!
  • Alex Hartford
    Alex Hartford about 3 years
    @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!