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.
Author by
witi
Updated on December 29, 2022Comments
-
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 usingIndexedStack
, however, that doesn't work in my case for two reasons:- The
Scaffold
in which the pages are displayed gets rebuilt when switching between pages because for some, but not all, pages theScaffold
should be extended:Scaffold( extendBody: _pageIndex == 1, ...)
- 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 theScaffold
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++; }); }, ), ); } }
- The
-
witi about 3 yearsThanks, Alex, this is helpful! I haven't looked into keys before and that just might do the trick for me!
-
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!