Looked at this feature before? Well, it’s rarely implemented in certain third-party apps and it has its uniqueness. So, let’s figure out how we can keep the Bottom Navigation Bar on every screen in this article. So, let’s get started.
I found this interesting concept in some social apps and thought about spreading this word to the amazing people out there. So, for your next app, try to implement this feature which we’re going to learn about in this post. (just a suggestion)
Table of Contents
For the initial setup, we need the following:
A class having a pre-configured routing setup with some additional methods
A separate class for the bottom navigation bar
Handling back press functionality with Willpopscope with some additional steps
Widget ingredients
After creating a new Flutter project and replacing the previous code with the main method and initial MyApp class.
First, create a new file as route_setup.dart
.
This class is responsible for:
- Requiring title, icon, widget, and its index as their initial parameter
- A GlobalKey<NavigatorState> for keeping track as well as a smooth routing process.
- Getter and setter for index
- Visibility widget to maintain the current state of the app
Here’s the code snippet
import 'package:flutter/material.dart';
class RouteSetp {
String title;
IconData icon;
Widget? _page;
int? _index;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
RouteSetp ({required this.title, required this.icon, required Widget? page}) {
_page = page;
}
/// Index getter and setters
set setIndex(i) => _index = i;
dynamic get getIndex => _index;
/// Visibility widget for maintaining state
Widget get page => Visibility(
visible: true,
maintainState: true,
child: Navigator(
key: navigatorKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(builder: (_) => _page!);
},
),
);
}
Under the above snippet, let’s deep down a little bit regarding the visibility widget’s content.
I have made the entity “visible: true” for now, but as we learn more, we shall make it dynamic too. Furthermore, I have made use of the Navigator widget to generate a runtime route for a particular screen that’s been passed from the previous page.
Let’s continue and create a new file home_view.dart
and jot down some code for BottomNavigationBar.
class HomeView extends StatelessWidget {
/// Three screens
final List<RouteSetp> tabs = [
RouteSetp(
title: "Quotes",
icon: Icons.list,
page: const Products(),
),
RouteSetp(
title: "Favorites",
icon: Icons.favorite_outline_sharp,
page: FavoritesView(),
),
RouteSetp(
title: "Settings",
icon: Icons.settings,
page: SettingsView()
),
];
/// To switch the indexes
static final BehaviorSubject<int> tapController =
BehaviorSubject<int>.seeded(0);
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
initialData: 0,
stream: tapController.stream,
builder: (context, snapshot) {
return Scaffold(
body: IndexedStack( /// To show single item per screen
index: snapshot.data!,
children: tabs.map((e) => e.page).toList(),
),
bottomNavigationBar: BottomNavigationBar(
elevation: 4,
currentIndex: snapshot.data!,
type: BottomNavigationBarType.fixed,
items: tabs.map((e) => item(e.title, e.icon)).toList(),
onTap: (i) => tapController.sink.add(i),
selectedItemColor: Theme.appPrimaryColor,
),
);
});
}
BottomNavigationBarItem item(String label, IconData icon) {
return BottomNavigationBarItem(
label: label,
icon: Icon(icon),
);
}
}
Note:
You might have noticed, I have used BehaviorSubject and treated it as a stream for the bottom navigation bar index.
BehaviorSubject is a part of rxDart (reactiveX) that is particularly used to capture the latest item added.
As per our scenario, we are listening to the latest value we get while switching to the index. We can also set the initial value (seed) if no value is provided.
Secondly, this looks more professional instead of the old setState
techniques we were fond of. This is the reason I chose this one to always make the best use of it.
IndexedStack
IndexStack is used to show a single child from a list of children [], w.r.t the index provided. The stack can be as big as the largest child. Furthermore, it auto-validates null values and displays nothing.
It requires two entities:
However, there are optional ones too.
Same as we have for Stack.
https://api.flutter.dev/flutter/widgets/IndexedStack-class.html
Now, coming back to the topic…
So, up till now, we’re halfway done, but our app is not yet ready for any sort of output, so let’s add some other configurations under the same file.
The following work needs to be done:
Setting the initial index under the initState method
Handling back press events with Willpopscope vis-à-vis preventing wrong route pop
At this point, convert the above class to Stateful Widget and jot down this code snippet:
void initState() {
tabs.asMap().forEach((key, value) {
value.setIndex = key;
});
super.initState();
}
Once this screen gets loaded, the first index gets set w.r.t the routes
Note: To retrieve the key (index), I have converted this method to asMap()'.
Handling back press events
Before we continue with this, let us first figure out what exactly we need to handle under onWillPop
.
Consider the following code snippet:
onWillPop: () async {
final isFirstRouteInCurrentTab = !await tabs[snapshot.data!]
.navigatorKey
.currentState!
.maybePop();
if (isFirstRouteInCurrentTab) {
if (snapshot.data! != 0) {
onSelect(snapshot.data!, 0); // Not yet implemented
return false;
}
}
return isFirstRouteInCurrentTab;
},
No need to worry, I’ll try my best to explain.
Here we are validating two conditions:
- A bool to check if the current route can pop, then check for index == 0, otherwise return to the previous route
- Additionally, a function checks for current index validation and pops out the routes in between until it reaches the first one.
Here’s the code snippet
dynamic onSelect(int currentIndex, int i) {
if (i == currentIndex) {
tabs[i].navigatorKey.currentState!.popUntil((route) => route.isFirst);
} else {
tapController.sink.add(currentIndex = i);
}
}
We’re almost done.
Let’s replace the line under the Visibility widget.
- Before
visible: true
- After
visible: true visible: _index == HomeView.tapController.value
At this point, we have completed all the tasks and our app is now ready to hit the ground.
Run the app and see the result.
Here, it’s time to conclude this article.
That’s all!
Wrapping Up
Hope you enjoyed every bit of it.
In this comprehensive blog post, we delved into the implementation of a Bottom Navigation Bar across multiple screens in Flutter. We not only discussed the theoretical aspects but also provided practical insights by showcasing relevant code snippets and relevant screenshots.
Through detailed explanations and visuals, we strived to equip developers with actionable knowledge that they can apply confidently in their upcoming Flutter projects, thereby fostering a deeper understanding of app navigation and also UI consistency principles.
Moreover, If you think I missed out on something or want to add your point of view, please jot it down in the comments section below. I would love to know that as well.
Furthermore, I would greatly appreciate it if you could propose an engaging Flutter topic suitable for an intriguing blog post.
Link to GitHub Repositories, and YouTube Channel
Read out previous blogs – Click here
Also, read Flutter’s Stateful Widget LifeCycle
Thanks for your precious time.