141

I'm currently working on building a Flutter app that will preserve states when navigating from one screen, to another, and back again when utilizing BottomNavigationBar. Just like it works in the Spotify mobile application; if you have navigated down to a certain level in the navigation hierarchy on one of the main screens, changing screen via the bottom navigation bar, and later changing back to the old screen, will preserve where the user were in that hierarchy, including preservation of the state.

I have run my head against the wall, trying various different things without success.

I want to know how I can prevent the pages in pageChooser(), when toggled once the user taps the BottomNavigationBar item, from rebuilding themselves, and instead preserve the state they already found themselves in (the pages are all stateful Widgets).

import 'package:flutter/material.dart';
import './page_plan.dart';
import './page_profile.dart';
import './page_startup_namer.dart';
void main() => runApp(new Recipher());
class Recipher extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
 return new Pages();
 }
}
class Pages extends StatefulWidget {
 @override
 createState() => new PagesState();
}
class PagesState extends State<Pages> {
 int pageIndex = 0;
 
 pageChooser() {
 switch (this.pageIndex) {
 case 0:
 return new ProfilePage();
 break;
 
 case 1:
 return new PlanPage();
 break;
 case 2:
 return new StartUpNamerPage(); 
 break; 
 
 default:
 return new Container(
 child: new Center(
 child: new Text(
 'No page found by page chooser.',
 style: new TextStyle(fontSize: 30.0)
 )
 ),
 ); 
 }
 }
 @override
 Widget build(BuildContext context) {
 return new MaterialApp(
 home: new Scaffold(
 body: pageChooser(),
 bottomNavigationBar: new BottomNavigationBar(
 currentIndex: pageIndex,
 onTap: (int tappedIndex) { //Toggle pageChooser and rebuild state with the index that was tapped in bottom navbar
 setState(
 (){ this.pageIndex = tappedIndex; }
 ); 
 },
 items: <BottomNavigationBarItem>[
 new BottomNavigationBarItem(
 title: new Text('Profile'),
 icon: new Icon(Icons.account_box)
 ),
 new BottomNavigationBarItem(
 title: new Text('Plan'),
 icon: new Icon(Icons.calendar_today)
 ),
 new BottomNavigationBarItem(
 title: new Text('Startup'),
 icon: new Icon(Icons.alarm_on)
 )
 ],
 )
 )
 );
 }
}
mako
1,36115 silver badges35 bronze badges
asked Mar 22, 2018 at 21:51
4
  • keep each pages using Stack. This might be helpful: stackoverflow.com/questions/45235570/… Commented Mar 23, 2018 at 2:47
  • Nice, thank you. This worked. I actually already tried to use a IndexedStack, but I now see that this does not work the same way as Offstage and Tickermode. Commented Mar 24, 2018 at 13:19
  • you got your answer yet because I want to do it same way as you mention Commented Jan 28, 2022 at 5:36
  • medium.com/@wartelski/… Commented Jul 21, 2024 at 8:57

12 Answers 12

130

Late to the party, but I've got a simple solution. Use the PageView widget with the AutomaticKeepAliveClientMixin.

The beauty of it that it doesn't load any tab until you click on it.


The page that includes the BottomNavigationBar:

var _selectedPageIndex;
List<Widget> _pages;
PageController _pageController;
@override
void initState() {
 super.initState();
 _selectedPageIndex = 0;
 _pages = [
 //The individual tabs.
 ];
 _pageController = PageController(initialPage: _selectedPageIndex);
}
@override
void dispose() {
 _pageController.dispose();
 super.dispose();
}
@override
Widget build(BuildContext context) {
 ...
 body: PageView(
 controller: _pageController,
 physics: NeverScrollableScrollPhysics(),
 children: _pages,
 ),
 bottomNavigationBar: BottomNavigationBar(
 ...
 currentIndex: _selectedPageIndex,
 onTap: (selectedPageIndex) {
 setState(() {
 _selectedPageIndex = selectedPageIndex;
 _pageController.jumpToPage(selectedPageIndex);
 });
 },
 ...
}

The individual tab:

class _HomeState extends State<Home> with AutomaticKeepAliveClientMixin<Home> {
 @override
 bool get wantKeepAlive => true;
 @override
 Widget build(BuildContext context) {
 //Notice the super-call here.
 super.build(context);
 ...
 }
}

I've made a video about it here.

Bharel
27.5k8 gold badges52 silver badges100 bronze badges
answered Sep 25, 2020 at 3:03
Sign up to request clarification or add additional context in comments.

7 Comments

Works really great thanks ! All _pages are not loaded at first like IndexedStack, but just saved.
Works perfectly... Each page loads separately and maintains state.
Neat 🤠 I was looking for this! Hope that the bottom navigation bar is not redrawn every time.
Best solution so far, thanks! IndexedStack loads everything at launch. It just loads as soon as you navigate to the pages and it just keeps states of the pages that have wantKeepAlive=true. Very useful if you just want to keep state of some of them but not all of them.
Love this answer, the IndexedStack was show the background of widgets in other tabs, but this solution doesn't have that issue. Keep up the good work!!
|
129

For keeping state in BottomNavigationBar, you can use IndexedStack

 @override
 Widget build(BuildContext context) {
 return Scaffold(
 bottomNavigationBar: BottomNavigationBar(
 onTap: (index) {
 setState(() {
 current_tab = index;
 });
 },
 currentIndex: current_tab,
 items: [
 BottomNavigationBarItem(
 ...
 ),
 BottomNavigationBarItem(
 ...
 ),
 ],
 ),
 body: IndexedStack(
 children: <Widget>[
 PageOne(),
 PageTwo(),
 ],
 index: current_tab,
 ),
 );
 }
answered Mar 20, 2020 at 13:50

12 Comments

But it has performance issue. IndexedStack loads all tab initially which is unnecessary.
This should be the accepted answer. Works great. @ArtBelch: You'll do us all a favor by marking this answer as the correct one. :)
@JayaPrakash Did you find a soltion for not building all stacks at once?
@anoop4real I am using IndexedStack to keep preserve the all widget state of tabs and i keep reference for each tab child reference. with on tab change callback i used to call any method inside the child widget with the help of tab child reference. If you need my answer then post a question, i will answer for that.
@JayaPrakash Regarding performance, take a look at this solution: stackoverflow.com/questions/66404759/…
|
49

Use AutomaticKeepAliveClientMixin to force your tab content to not be disposed.

class PersistantTab extends StatefulWidget {
 @override
 _PersistantTabState createState() => _PersistantTabState();
}
class _PersistantTabState extends State<PersistantTab> with AutomaticKeepAliveClientMixin {
 @override
 Widget build(BuildContext context) {
 return Container();
 }
 // Setting to true will force the tab to never be disposed. This could be dangerous.
 @override
 bool get wantKeepAlive => true;
}

To make sure your tab does get disposed when it doesn't require to be persisted, make wantKeepAlive return a class variable. You must call updateKeepAlive() to update the keep alive status.

Example with dynamic keep alive:

// class PersistantTab extends StatefulWidget ...
class _PersistantTabState extends State<PersistantTab>
 with AutomaticKeepAliveClientMixin {
 bool keepAlive = false;
 @override
 void initState() {
 doAsyncStuff();
 }
 Future doAsyncStuff() async {
 keepAlive = true;
 updateKeepAlive();
 // Keeping alive...
 await Future.delayed(Duration(seconds: 10));
 keepAlive = false;
 updateKeepAlive();
 // Can be disposed whenever now.
 }
 @override
 bool get wantKeepAlive => keepAlive;
 @override
 Widget build(BuildContext context) {
 super.build(context);
 return Container();
 }
}
Vinoth
9,8193 gold badges73 silver badges75 bronze badges
answered Aug 8, 2018 at 3:35

2 Comments

to whom we should conform with AutomaticKeepAliveClientMixin the parent widget or the widget that will be shown when button is pressed?
From the docs: their build methods must call super.build, which is missing in your answer.
15

Instead of returning new instance every time you run pageChooser, have one instance created and return the same.

Example:

class Pages extends StatefulWidget {
 @override
 createState() => new PagesState();
}
class PagesState extends State<Pages> {
 int pageIndex = 0;
 // Create all the pages once and return same instance when required
 final ProfilePage _profilePage = new ProfilePage(); 
 final PlanPage _planPage = new PlanPage();
 final StartUpNamerPage _startUpNamerPage = new StartUpNamerPage();
 Widget pageChooser() {
 switch (this.pageIndex) {
 case 0:
 return _profilePage;
 break;
 case 1:
 return _planPage;
 break;
 case 2:
 return _startUpNamerPage;
 break;
 default:
 return new Container(
 child: new Center(
 child: new Text(
 'No page found by page chooser.',
 style: new TextStyle(fontSize: 30.0)
 )
 ),
 );
 }
 }
 @override
 Widget build(BuildContext context) {
 return new MaterialApp(
 home: new Scaffold(
 body: pageChooser(),
 bottomNavigationBar: new BottomNavigationBar(
 currentIndex: pageIndex,
 onTap: (int tappedIndex) { //Toggle pageChooser and rebuild state with the index that was tapped in bottom navbar
 setState(
 (){ this.pageIndex = tappedIndex; }
 );
 },
 items: <BottomNavigationBarItem>[
 new BottomNavigationBarItem(
 title: new Text('Profile'),
 icon: new Icon(Icons.account_box)
 ),
 new BottomNavigationBarItem(
 title: new Text('Plan'),
 icon: new Icon(Icons.calendar_today)
 ),
 new BottomNavigationBarItem(
 title: new Text('Startup'),
 icon: new Icon(Icons.alarm_on)
 )
 ],
 )
 )
 );
 }
}

Or you can make use of widgets like PageView or Stack to achieve the same.

Hope that helps!

answered Mar 23, 2018 at 5:37

3 Comments

Thank you for your answer. I solved the problem using a Stack combined with Offstage and Tickermode. :-))
@ArtBelch, if the answer satisfy your question, please mark it as correct :)
@ArtBelch could you share your solution please?
10

Use "IndexedStack Widget" with "Bottom Navigation Bar Widget" to keep state of Screens/pages/Widget

Provide list of Widget to IndexedStack and index of widget you want to show because IndexedStack show single widget from list at one time.

final List<Widget> _children = [
 FirstClass(),
 SecondClass()
 ];
Scaffold(
 body: IndexedStack(
 index: _selectedPage,
 children: _children,
 ),
 bottomNavigationBar: BottomNavigationBar(
 ........
 ........
 ), 
);
answered Jul 24, 2020 at 16:49

Comments

5

The most convenient way I have found to do so is using PageStorage widget along with PageStorageBucket, which acts as a key value persistent layer.

Go through this article for a beautiful explanation -> https://steemit.com/utopian-io/@tensor/persisting-user-interface-state-and-building-bottom-navigation-bars-in-dart-s-flutter-framework

answered Jun 8, 2019 at 7:19

1 Comment

In this case, my pages are still loading data
3

Do not use IndexStack Widget, because it will instantiate all the tabs together, and suppose if all the tabs are making a network request then the callbacks will be messed up the last API calling tab will probably have the control of the callback.

Use AutomaticKeepAliveClientMixin for your stateful widget it is the simplest way to achieve it without instantiating all the tabs together.

My code had interfaces that were providing the respective responses to the calling tab I implemented it the following way.

Create your stateful widget

class FollowUpsScreen extends StatefulWidget {
 FollowUpsScreen();
 
 @override
 State<StatefulWidget> createState() {
 return FollowUpsScreenState();
 }
}
 
class FollowUpsScreenState extends State<FollowUpsScreen>
 with AutomaticKeepAliveClientMixin<FollowUpsScreen>
 implements OperationalControls {
 
 @override
 Widget build(BuildContext context) {
 //do not miss this line
 super.build(context);
 return .....;
 }
 @override
 bool get wantKeepAlive => true;
}
MahMoos
1,34612 silver badges18 bronze badges
answered Jul 19, 2021 at 19:28

Comments

2

Summary of Solutions: Two Approaches Offered

Solution 1: AutomaticKeepAliveClientMixin

class SubPage extends StatefulWidget {
 @override
 State<SubPage> createState() => _SubPageState();
}
class _SubPageState extends State<SubPage> with AutomaticKeepAliveClientMixin {
 @override
 Widget build(BuildContext context) {
 super.build(context); // Ensure that the mixin is initialized
 return Container();
 }
 @override
 bool get wantKeepAlive => true;
}
/*-------------------------------*/
class MainPage extends StatefulWidget {
 const MainPage({Key? key}) : super(key: key);
 @override
 State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
 int currentBottom = 0;
 static const List<Widget> _page = [
 SubPage(),
 SubPage(),
 ];
 @override
 Widget build(BuildContext context) {
 return Scaffold(
 bottomNavigationBar: BottomNavigationBar(
 currentIndex: currentBottom,
 onTap: (index) => setState(() {
 currentBottom = index;
 }),
 items: const [
 BottomNavigationBarItem(icon: Icon(Icons.home)),
 BottomNavigationBarItem(icon: Icon(Icons.settings))
 ],
 ),
 body: _page.elementAt(currentBottom));
 }
}

Solution 2: IndexedStack

class MainPage extends StatefulWidget {
 @override
 _MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
 int _currentIndex = 0;
 @override
 Widget build(BuildContext context) {
 return Scaffold(
 body: IndexedStack(
 index: _currentIndex,
 children: <Widget>[
 Page1(),
 Page2(),
 Page3(),
 ],
 ),
 bottomNavigationBar: BottomNavigationBar(
 currentIndex: _currentIndex,
 onTap: (int index) {
 setState(() {
 _currentIndex = index;
 });
 },
 items: [
 BottomNavigationBarItem(
 icon: Icon(Icons.home),
 label: 'Page 1',
 ),
 BottomNavigationBarItem(
 icon: Icon(Icons.search),
 label: 'Page 2',
 ),
 BottomNavigationBarItem(
 icon: Icon(Icons.settings),
 label: 'Page 3',
 ),
 ],
 ),
 );
 }
}
/*--------------------------------*/
class Page1 extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
 return Container(
 // Page 1 content
 );
 }
}
// Repeat the above for Page2 and Page3
answered Jul 13, 2023 at 8:15

Comments

1

This solution is based on CupertinoTabScaffold's implementation which won't load screens unnecessary.

import 'package:flutter/material.dart';
enum MainPage { home, profile }
class BottomNavScreen extends StatefulWidget {
 const BottomNavScreen({super.key});
 @override
 State<BottomNavScreen> createState() => _BottomNavScreenState();
}
class _BottomNavScreenState extends State<BottomNavScreen> {
 var currentPage = MainPage.home;
 @override
 Widget build(BuildContext context) {
 return Scaffold(
 body: PageSwitchingView(
 currentPageIndex: MainPage.values.indexOf(currentPage),
 pageCount: MainPage.values.length,
 pageBuilder: _pageBuilder,
 ),
 bottomNavigationBar: BottomNavigationBar(
 currentIndex: MainPage.values.indexOf(currentPage),
 onTap: (index) => setState(() => currentPage = MainPage.values[index]),
 items: const [
 BottomNavigationBarItem(
 label: 'Home',
 icon: Icon(Icons.home),
 ),
 BottomNavigationBarItem(
 label: 'Profile',
 icon: Icon(Icons.account_circle),
 ),
 ],
 ),
 );
 }
 Widget _pageBuilder(BuildContext context, int index) {
 final page = MainPage.values[index];
 switch (page) {
 case MainPage.home:
 return ...
 case MainPage.profile:
 return ...
 }
 }
}
/// A widget laying out multiple pages with only one active page being built
/// at a time and on stage. Off stage pages' animations are stopped.
class PageSwitchingView extends StatefulWidget {
 const PageSwitchingView({
 super.key,
 required this.currentPageIndex,
 required this.pageCount,
 required this.pageBuilder,
 });
 final int currentPageIndex;
 final int pageCount;
 final IndexedWidgetBuilder pageBuilder;
 @override
 State<PageSwitchingView> createState() => _PageSwitchingViewState();
}
class _PageSwitchingViewState extends State<PageSwitchingView> {
 final List<bool> shouldBuildPage = <bool>[];
 @override
 void initState() {
 super.initState();
 shouldBuildPage.addAll(List<bool>.filled(widget.pageCount, false));
 }
 @override
 void didUpdateWidget(PageSwitchingView oldWidget) {
 super.didUpdateWidget(oldWidget);
 // Only partially invalidate the pages cache to avoid breaking the current
 // behavior. We assume that the only possible change is either:
 // - new pages are appended to the page list, or
 // - some trailing pages are removed.
 // If the above assumption is not true, some pages may lose their state.
 final lengthDiff = widget.pageCount - shouldBuildPage.length;
 if (lengthDiff > 0) {
 shouldBuildPage.addAll(List<bool>.filled(lengthDiff, false));
 } else if (lengthDiff < 0) {
 shouldBuildPage.removeRange(widget.pageCount, shouldBuildPage.length);
 }
 }
 @override
 Widget build(BuildContext context) {
 return Stack(
 fit: StackFit.expand,
 children: List<Widget>.generate(widget.pageCount, (int index) {
 final active = index == widget.currentPageIndex;
 shouldBuildPage[index] = active || shouldBuildPage[index];
 return HeroMode(
 enabled: active,
 child: Offstage(
 offstage: !active,
 child: TickerMode(
 enabled: active,
 child: Builder(
 builder: (BuildContext context) {
 return shouldBuildPage[index] ? widget.pageBuilder(context, index) : Container();
 },
 ),
 ),
 ),
 );
 }),
 );
 }
}
answered Oct 26, 2022 at 16:51

2 Comments

Yes, it inded keeps state. I have an opposite question: I am using CupertinoTabScaffold but I dont want to keep state. Is there any workaround?
Provide your own CupertinoTabController and listen to changes so when index changes provide an incremented Key to your tabBuilder's child.
1

You can use a Lazy IndexedStack.
Normally, an IndexedStack loads all its child pages at once, which is not very good practice.

You can load pages only when needed by maintaining a list of loaded pages indexes and conditionally load pages as the user navigates between tabs.

class Home extends StatefulWidget {
 const Home({super.key});
 @override
 State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
 int _currentTab = 0;
 final List<int> loadedPages = [0];
 @override
 Widget build(BuildContext context) {
 return Scaffold(
 bottomNavigationBar: BottomNavBar(
 currentTab: _currentTab,
 onTap: (index){
 if (!loadedPages.contains(index)) {
 loadedPages.add(index);
 }
 setState(() {
 _currentTab = index;
 });
 },
 ),
 body: IndexedStack(
 index: _currentTab,
 children: [
 loadedPages.contains(0)
 ? const FirstTab()
 : const SizedBox.shrink(),
 loadedPages.contains(1)
 ? const SecondTab()
 : const SizedBox.shrink(),
 loadedPages.contains(2)
 ? const ThirdTab()
 : const SizedBox.shrink(),
 )
 ],
 ),
 );
 }
}
answered Jul 27, 2024 at 16:05

Comments

0

proper way of preserving tabs state in bottom nav bar is by wrapping the whole tree with PageStorage() widget which takes a PageStorageBucket bucket as a required named parameter and for those tabs to which you want to preserve its state pas those respected widgets with PageStorageKey(<str_key>) then you are done !! you can see more details in this ans which i've answered few weeks back on one question : https://stackoverflow.com/a/68620032/11974847

there's other alternatives like IndexedWidget() but you should beware while using it , i've explained y we should be catious while using IndexedWidget() in the given link answer

good luck mate ..

answered Sep 23, 2021 at 15:42

Comments

0

All the examples here are using BottomNavigationBar which has now (over 6 years since the question was asked - which admittedly refers to specifically to BottomNavigationBar) been replaced for Material 3 by NavigationBar, and none of them seem to use a PageTransitionSwitcher, which to me is a default requirement for any app I build.

PageStorage is what I use to fulfil the OP's needs. Even the official Flutter documentation example of this uses an older BottomNavigationBar so I decided to share my implementation with the newer NavigationBar and a lovely FadeThroughTransition in this blog post.

answered Dec 18, 2024 at 19:32

1 Comment

I wasn't able to get this to work when using flutter_adaptive_scaffold

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.