Old dog, new Flutter trick (Part 4)

Pieter Venter
DVT Software Engineering
11 min readAug 18, 2021

Another article, more tricks, Flutter and new widgets. Sound good?

In my previous articles on this channel, we’ve had a look at various Flutter concepts. Our journey has been long and hard and hopefully, you’ve learned something along the way. For this article, I thought it would be a good idea to have a look at bottom navigation to see how we can show even more content in our apps. We’ll definitely be having a look at a few cool tips and tricks along the way, so let’s get started.

As always, you can find all the code available on Github. Let’s get going.

So, why specifically bottom navigation? Well, you can have a look at the material design guidelines if you’re unsure of when to use it, but it’s a helpful and simple way of giving your user access to a bunch of key features and makes navigation around your app easy. I wouldn’t say that all apps should use bottom navigation, but I’d say in most cases it’s a viable option for you to consider.

How do you add bottom navigation to Flutter apps?

Standard setup as usual, let’s clear out our main.dart:

void main() {
runApp( MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}

And we’ll create our standard home page setup as always:

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("The Flutter Series: Part 4"),
centerTitle: true,
),
body: Container(),
);
}
}

Now we’re ready to start our next adventure.

Flutter actually makes it super easy for us to add our own bottom navigation, thanks to another component you might have forgotten about at this point.

Remember how we said that most apps have a common structure to them?

No?

That’s fine, well, if you consider your favourite apps, they usually have a common structure to them — we’ve been using Scaffold to help us apply an appBar and a body to our apps, but now we’ll be using it to help us out with our bottom navigation.

Adding bottom navigation to our app is really quite simple. All we have to do is add it to our scaffold:

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("The Flutter Series: Part 4"),
centerTitle: true,
),
body: Container(),
bottomNavigationBar: BottomNavigationBar(),
);
}
}

A bottom navigation bar takes an array of navigation items, so to add some basic and random items to our app, we can do something like this:

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("The Flutter Series: Part 4"),
centerTitle: true,
),
body: Container(),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.home,
),
label: "Home",
),
BottomNavigationBarItem(
icon: Icon(
Icons.audiotrack_outlined,
),
label: "Music"),
BottomNavigationBarItem(
icon: Icon(
Icons.settings,
),
label: "Settings",
),
],
),
);
}
}

Flutter gives us a whole bunch of default icons we can use, so we’ll use these just as an example. Running our app at this point, you’ll see the following:

Bottom navigation sorted.

Awesome! We have a bottom navigation bar now. That was pretty simple to make, right?

At the moment, it doesn’t do anything, so let’s see how we can add some functionality to it.

If we wanted our UI to update according to a change a user made, then that doesn’t sound like a stateless widget anymore, right? If we consider the fact that our bottom navigation will be showing a selected item that can change, we can definitely start to understand that we’ll have to make use of a stateful widget. No problem. Let’s get to work.

So, to make things easy, let’s move our Bottom navigation to a new file.

We’ll create a new file called dashboard_navigation_bar.dart (you can call it whatever you want) and let’s move all our previous code over to the new file:

class DashboardNavigationBar extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return DashboardNavigationBarState();
}
}

class DashboardNavigationBarState extends State<DashboardNavigationBar> {
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.home,
),
label: "Home",
),
BottomNavigationBarItem(
icon: Icon(
Icons.audiotrack_outlined,
),
label: "Music"),
BottomNavigationBarItem(
icon: Icon(
Icons.settings,
),
label: "Settings",
),
],
);
}
}

It’s just a standard stateful widget — nothing too complicated or strange here. Let’s make sure we hook it up to our home page:

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("The Flutter Series: Part 4"),
centerTitle: true,
),
body: Container(),
bottomNavigationBar: DashboardNavigationBar(),
);
}
}

Okay, cool. Now we have a stateful widget that represents our bottom navigation, but how do we change it? Well, a BottomNavigationBar works with a parameter called currentIndex, so if you wanted to have a different item selected, here’s what you’d do:

class DashboardNavigationBarState extends State<DashboardNavigationBar> {
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: 1,
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.home,
),
label: "Home",
),
BottomNavigationBarItem(
icon: Icon(
Icons.audiotrack_outlined,
),
label: "Music"),
BottomNavigationBarItem(
icon: Icon(
Icons.settings,
),
label: "Settings",
),
],
);
}
}

You’d simply change the currentIndex parameter. This means that, in order for us to change what is selected in our bottom navigation, we have to change the currentIndex value. By default it’s set to 0.

So, we can create a variable and change that variable as we change on tabs, right?

Inside of our DashboardNavigationBarState, we can declare a simple variable to keep track of the index:

class DashboardNavigationBarState extends State<DashboardNavigationBar> {
int _selectedIndex = 0;

@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: _selectedIndex,

OK, so that’s done, but how or where do we now change our variable?

Let me show you how to do it, then I’ll explain.

class DashboardNavigationBarState extends State<DashboardNavigationBar> {
int _selectedIndex = 0;

@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.home,
),
label: "Home",
),
BottomNavigationBarItem(
icon: Icon(
Icons.audiotrack_outlined,
),
label: "Music"),
BottomNavigationBarItem(
icon: Icon(
Icons.settings,
),
label: "Settings",
),
],
);
}
}

OK, let’s go through it, what’s happening here?

onTap: (index) {
setState(() {
_selectedIndex = index;
});
},

Well, in order for us to manage the selected tab, we have to add the onTap listener. What does onTap do? It provides us with a useful value — it tells us exactly which index was tapped. All we have to do is update our _selectedIndex variable and call setState so that we can update our widget.

That’s it! Our bottom navigation will now update according to where we tap.

So, at this point, we have a working bottom navigation, but we haven’t really looked at how we can change the content of our app based on what was tapped, right?

Let’s continue our adventure.

As soon as you start to consider that our app will be changing, it means we need to convert to a stateful widget, right? Our current home page is a stateless widget, we’ll have to update it to a stateful widget in order for us to change the content we’ll be showing, so let’s change it to stateful quickly:

class MyHomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return MyHomePageState();
}
}
class MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("The Flutter Series: Part 4"),
centerTitle: true,
),
body: Container(),
bottomNavigationBar: DashboardNavigationBar(),
);
}
}

Simple enough. This means that our page can now change. But, we haven’t really looked at how we’d be changing the content. It makes sense that our content will change based on the index of the tab selected, so we can start by declaring a variable which we will use to change the content.

class MyHomePageState extends State<MyHomePage> {
int _page = 0;

Based on what this variable is, we will be showing a different widget. How would we write this?

Let’s learn about some more concepts.

We’ll be creating a function to help us out here and this function will be returning a widget, based on the page.

What’s a function?

At the most basic level, a function is a collection of code that should, ideally, be doing one thing at a time. Functions help us to arrange our code in a way where we don’t just write millions and millions of lines repeatedly because we can reuse our functions instead.

Let’s build our first function. We want our function to return a widget, so this is how we’ll define it:

Widget _bodyFunction() {
switch (_page) {
case 0:
return Text("First page");
break;
case 1:
return Text("Second page");
break;
case 2:
return Text("Third page");
break;
default:
return Container(color: Colors.white);
break;
}
}

What’s happening here? We wrote a function called bodyFunction (The underscore just makes it private, meaning we can only use it in this file). This function of ours will be returning a widget, so that’s why we have Widget at the start of our function. What’s happening inside our function? We are making use of a switch statement to determine what is returned. If our page variable is 0, we will return a simple Text widget saying First Page and if our page variable is 1, we’ll return something else. I’m making use of a simple Text widget just to show that our content changes, but you can use anything you want here, it could be a complete screen with a whole bunch of other widgets if that’s what you’d like.

Where will we make use of this function?

Because this function is responsible for determining the content of the app, we’re going to use it to build the body of our app, so we’ll update the body of our scaffold like this:

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("The Flutter Series: Part 4"),
centerTitle: true,
),
body: _bodyFunction(),
bottomNavigationBar: DashboardNavigationBar(),
);
}

So this means that the content of our app will be determined by our function, and our function will change based on the value of our _page variable. Now, all we have to do is find a way to update this variable based on our bottom navigation, right?

We’re going to make use of one more concept in this article and then our journey will be done. In order for us to update our _page variable, we’re going to need to pass a value back from our bottom navigation when it changes. To make this possible, we’re going to make use of our own callback function.

Similar to how we can pass variables to widgets, we can even pass around our own functions. Why would we have to do that? When passing in functions, we can trigger these functions to pass back values to components when certain conditions have been met. In our case, we’re going to be passing back the value of the selected bottom navigation index, once a user has tapped on our bottom navigation.

Let’s get started.

Let’s change up our dashboard widget. It now has to take in a function as a parameter, so this is how you’d add it:

class DashboardNavigationBar extends StatefulWidget {

final Function _callback;

@override
State<StatefulWidget> createState() {
return DashboardNavigationBarState(_callback);
}
DashboardNavigationBar(this._callback);
}

We’ve defined it as a variable and added it to our constructor. In order for us to get this into our DashboardNavigationBarState class, we need to update it to accommodate for this value as well:

class DashboardNavigationBarState extends State<DashboardNavigationBar> {
int _selectedIndex = 0;

Function _callback;

DashboardNavigationBarState(this._callback);

Currently though, we’re not making use of this callback, so we need to trigger it from somewhere. We’re going to use it to update our app once a user taps on the navigation, then, the navigation will give us the index of the item which was tapped, so let’s make use of that and update our onTap:

onTap: (index) {
setState(() {
_selectedIndex = index;
_callback(index);
});
}

This means that we’re going to be using this callback once a user taps on our navigation.

Back in our my_home_page.dart file, we now need to define how we’re going to define what should happen when the function from our bottom navigation is triggered. We’re going to define our function like this :

void _bottomNavTapped(int index) {
setState(() {
_page = index;
});
}

This function will tell our home page to update because we’re making use of setState. We’re updating our local _page variable so that the content of our app will change accordingly. The void keyword is just a way of saying that our function won’t be returning anything, which makes sense because we’re just updating a bunch of stuff here, but we don’t want a value back from this function.

That’s it. If you run the app now, your app will update as you tap on the bottom navigation. The content of your app will change based on what you’ve defined in your _bodyFunction, so you can change it as you need to.

For anyone who hasn’t heard of Flutter before, Flutter is an open source Software Development Kit (SDK) backed by Google — a free framework that allows you to write code and deploy it to various different platforms — mobile, web and desktop too, all from a single codebase. Not only does the Flutter framework allow you to develop beautiful looking mobile apps, it also keeps native performance in mind, by making sure that everything works as you’d expect it to, for your specific platform and hardware.

Flutter makes use of Dart, which enables compilation to 32-bit and 64-bit ARM machine code for iOS and Android, as well as JavaScript for the web and Intel x64 for desktop devices. If you’ve used something like Kotlin, Swift or even Javascript before, you should be able to learn Dart really quickly.

Flutter includes the contributions of hundreds of developers from around the world and has a vibrant ecosystem of thousands of plug-ins. Every Flutter app is a native app that uses the standard Android and iOS build tools, therefore you can access everything from the underlying operating system, including code and UIs written in Kotlin or Java on Android, and Swift or Objective-C on iOS. Put this all together, combine it with best-in-class tooling for Visual Studio Code, Android Studio, IntelliJ or the programmer’s editor of your choice, and you have Flutter — a development environment for building beautiful native experiences for iOS or Android from a single codebase.

Flutter has been adopted by the broader community quite quickly, as evidenced by the thousands of Flutter apps that are already published to the Apple and Google Play stores. It’s clear that developers are ready for a new approach to UI development in order to create impressive iOS and Android apps.

So in a nutshell, Flutter makes it simple to deliver quality apps to any platform you want — not only that, but it allows you to do this really quickly, with techniques specifically designed to boost productivity, such as allowing you to quickly reload to see the changes you’ve made. You can have a look here if you’re interested in learning more. If you’ve ever considered something like React Native, perhaps you should give Flutter a chance as well.

--

--