I am trying to use BottomNavigator
with Nested Navigation for each tab along with preserving the state. I am using Pageview
for the displaying the pages. Each page is a Navigator widget has its own navigation stack. The issue here is - in each Navigator, the first page is always rebuilt but the second page is built only once. To prevent the first page from rebuilding I have used AutomaticKeepAliveClientMixin
and it works. But I don't get why only the first widget is getting rebuilt and not the second one in the Navigator when I switch the tabs.
import 'package:flutter/material.dart';
void main() {
runApp(const BottomNavBarWithPageView());
}
final navKeyBooks = GlobalKey<NavigatorState>();
final navKeyCoffee = GlobalKey<NavigatorState>();
final navKeyMovies = GlobalKey<NavigatorState>();
final navKeys = [navKeyBooks, navKeyCoffee, navKeyMovies];
var buildText = '';
toast(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(buildText),
duration: const Duration(milliseconds: 400),
));
}
class BottomNavBarWithPageView extends StatelessWidget {
const BottomNavBarWithPageView({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bottom Nav Bar',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
useMaterial3: false),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late int _selectedIndex;
late List<Widget> _pages;
late PageController _pageController;
@override
void initState() {
_selectedIndex = 0;
_pages = const [BooksNavigator(), CoffeeNavigator(), MovieNavigator()];
_pageController = PageController(initialPage: _selectedIndex);
super.initState();
}
Future<bool> _systemBackButtonPressed() {
if (navKeys[_selectedIndex].currentState!.canPop()) {
navKeys[_selectedIndex]
.currentState!
.pop(navKeys[_selectedIndex].currentContext);
return Future.value(false);
} else {
// SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop');
return Future.value(true);
}
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () => _systemBackButtonPressed(),
child: Scaffold(
appBar: AppBar(
title: const Text('Bottom Nav Bar PageView'),
leading: const BackButton(
color: Colors.black,
),
),
body: PageView(
controller: _pageController,
//The following parameter is just to prevent
//the user from swiping to the next page.
physics: const NeverScrollableScrollPhysics(),
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.book),
label: 'Books',
),
BottomNavigationBarItem(
icon: Icon(Icons.coffee),
label: 'Coffee',
),
BottomNavigationBarItem(
icon: Icon(Icons.movie),
label: 'Movie',
),
],
currentIndex: _selectedIndex,
onTap: (value) {
setState(() {
_selectedIndex = value;
_pageController.jumpToPage(_selectedIndex);
});
},
),
),
);
}
}
class BooksNavigator extends StatelessWidget {
const BooksNavigator({super.key});
@override
Widget build(BuildContext context) {
return Navigator(
key: navKeyBooks,
initialRoute: '/',
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case '/':
return const Books1();
case '/2':
return const Books2();
default:
print('Books Error');
return const Center(child: Text('Books Error'));
}
});
},
);
}
}
class Books1 extends StatefulWidget {
const Books1({super.key});
@override
State<Books1> createState() => _Books1State();
}
class _Books1State extends State<Books1>
with AutomaticKeepAliveClientMixin<Books1> {
int counter = 0;
@override
Widget build(BuildContext context) {
print('Books1 build');
buildText = 'Books1 build';
WidgetsBinding.instance.addPostFrameCallback((_) => toast(context));
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Books1 - $counter'),
const SizedBox(
height: 16,
),
ElevatedButton(
onPressed: () {
setState(() {
counter++;
});
},
child: const Text('Increment'),
),
const SizedBox(
height: 16,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed('/2');
},
child: const Text('Next Page'),
)
],
),
);
}
@override
bool get wantKeepAlive => true;
}
class Books2 extends StatelessWidget {
const Books2({super.key});
@override
Widget build(BuildContext context) {
print('Books2 build');
buildText = 'Books2 build';
WidgetsBinding.instance.addPostFrameCallback((_) => toast(context));
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Books2'),
const SizedBox(
height: 16,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Back'),
)
],
),
);
}
}
class CoffeeNavigator extends StatelessWidget {
const CoffeeNavigator({super.key});
@override
Widget build(BuildContext context) {
return Navigator(
key: navKeyCoffee,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case '/':
return const Coffee1();
case '/2':
return const Coffee2();
default:
return const Center(child: Text('Coffee Error'));
}
});
},
);
}
}
class Coffee1 extends StatelessWidget {
const Coffee1({super.key});
@override
Widget build(BuildContext context) {
print('Coffee1 build');
buildText = 'Coffee1 build';
WidgetsBinding.instance.addPostFrameCallback((_) => toast(context));
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Coffee1'),
const SizedBox(
height: 16,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed('/2');
},
child: const Text('Next Page'),
)
],
),
);
}
}
class Coffee2 extends StatelessWidget {
const Coffee2({super.key});
@override
Widget build(BuildContext context) {
print('Coffee2 build');
buildText = 'Coffee2 build';
WidgetsBinding.instance.addPostFrameCallback((_) => toast(context));
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Coffee2'),
const SizedBox(
height: 16,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Back'),
)
],
),
);
}
}
class MovieNavigator extends StatelessWidget {
const MovieNavigator({super.key});
@override
Widget build(BuildContext context) {
return Navigator(
key: navKeyMovies,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case '/':
return const Movie1();
case '/2':
return const Movie2();
default:
return const Center(child: Text('Movie Error'));
}
});
},
);
}
}
class Movie1 extends StatelessWidget {
const Movie1({super.key});
@override
Widget build(BuildContext context) {
print('Movie1 build');
buildText = 'Movie1 build';
WidgetsBinding.instance.addPostFrameCallback((_) => toast(context));
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Movie1'),
const SizedBox(
height: 16,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed('/2');
},
child: const Text('Next Page'),
)
],
),
);
}
}
class Movie2 extends StatelessWidget {
const Movie2({super.key});
@override
Widget build(BuildContext context) {
print('Movie2 build');
buildText = 'Movie2 build';
WidgetsBinding.instance.addPostFrameCallback((_) => toast(context));
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Movie2'),
const SizedBox(
height: 16,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Back'),
)
],
),
);
}
}
I was expecting that all the pages maintain the state when the tabs are switched.