How To Build a Custom BottomAppBar in Flutter

Jonah Walker
4 min readMar 8, 2019

including a large tap area around an SVG

Photo by Jordan on Unsplash

Let’s get straight to it. I was having a hard time finding good examples on how to build a custom app bar. Here is my solution. Please provide comments if this helped you or you have any suggestions on improvements!

Dependencies:
- flutter_svg: https://pub.dartlang.org/packages/flutter_svg

Goals:

  1. Create a bottom nav with 3 icons spaced evenly
  2. Change the background color of the active link
  3. Make the icons use svg
  4. Have a strongly typed routing system (not needed for the navbar but is in the example because it’s what I use)

Code!

…it’s not as scary as it looks.

// Utilities located in other files
enum RouteType {
freeTime,
splashScreen,
feed,
goal,
milestone,
login,
wheelOfTimeManagement,
none
}
class RouteId {
static String from(RouteType routeType) {
bool isDefaultRoute = routeType == RouteType.feed;
String routeName = isDefaultRoute
? ''
: describeEnum(routeType);
return '/$routeName';
}
}
// Code located in main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
RouteType _selectedRoute;
MaterialApp(
title: //...
theme: //...
routes: {
RouteId.from(RouteType.feed): (context) {
_selectedRoute = RouteType.feed;
// ScaffoldWithBottomAppBar...
},
RouteId.from(RouteType.splashScreen): (context) {
_selectedRoute = RouteType.splashScreen;
// ScaffoldWithBottomAppBar...
},
RouteId.from(RouteType.login): (context) {
_selectedRoute = RouteType.login;
// ScaffoldWithBottomAppBar...
},
RouteId.from(RouteType.milestone): (context) {
_selectedRoute = RouteType.milestone;
// ScaffoldWithBottomAppBar...
},
RouteId.from(RouteType.goal): (context) {
_selectedRoute = RouteType.goal;
// ScaffoldWithBottomAppBar...
},
RouteId.from(RouteType.wheelOfTimeManagement): (context) {
_selectedRoute = RouteType.wheelOfTimeManagement;
// ScaffoldWithBottomAppBar...
},
RouteId.from(RouteType.freeTime): (context) {
_selectedRoute = RouteType.freeTime;
// ScaffoldWithBottomAppBar...
},
},
);
Widget _bottomAppBar() {
return BottomAppBar(
child: Builder(builder: (context) {
return GestureDetector(
behavior: HitTestBehavior.deferToChild,
child: Row(
children: <Widget>[
Expanded(
child: _buildNavIcon(
RouteType.freeTime,
_selectedRoute
),
),
Expanded(
child: _buildNavIcon(
RouteType.feed,
_selectedRoute
),
),
Expanded(
child: _buildNavIcon(
RouteType.goal,
_selectedRoute
),
),
],
),
onTap: () {
// tap is padded through to child
},
);
}),
);
}
Widget _buildNavIcon(RouteType navigationScreen, RouteType selectedRoute) {
double iconHeight = 30.0;
bool isSelected = navigationScreen != selectedRoute;
Widget _iconLogo;
switch (navigationScreen) {
case RouteType.freeTime:
_iconLogo = SvgPicture.asset(
'assets/clock.svg',
semanticsLabel: 'Schedule Screen',
height: iconHeight,
);
break;
case RouteType.feed:
_iconLogo = SvgPicture.asset(
'assets/confetti.svg',
semanticsLabel: 'Feed Screen',
height: iconHeight,
);
break;
case RouteType.goal:
_iconLogo = SvgPicture.asset(
'assets/plus.svg',
semanticsLabel: 'Goals Screen',
height: iconHeight,
);
break;
default:
}
return Builder(
builder: (context) => GestureDetector(
child: Container(
child: _iconLogo,
padding: EdgeInsets.all(8.0),
color: isSelected
? Theme.of(context).accentColor
: Theme.of(context).highlightColor,
),
onTap: () => Navigator.pushNamed(
context,
RouteId.from(navigationScreen),
),
),
);
}
}

Whew, alright. So now for the explanation.

Explanation

I’m just going to do a high level overview here, feel free to ask if you have any questions.

RouteType is an enum used for making my life easier when it comes to knowing what routes I can navigate to from other pages.

RouteId.from is a static method used to define my default route (because it returns / for the enum specified) and converts the RouteType argument into a string needed when using named routes.

MyApp is stateful because you need to be able to update the active link when the user taps the screen.

routes runs first and needs to set the _selectedRoute since we’re not able to get access to the current route through the Navigator. Once this is set, the _bottomAppBar is rendered within the Scaffold.

_bottomAppBar builds out the navbar to be used on the Scaffold and consists of a Row with 3 Expanded widgets nested inside of it. In order to recognize interactions I wrapped the Row with a GestureDetector. This allows for you to have a large tap area consisting of the entire app bar. The onTap argument is required by the GestureDetector widget but is not directly used thanks to the behavior we set. Setting the behavior to HitTestBehavior.deferToChild tells the GestureDetector to bypass its own onTap function and instead pass the tap through to its child.

_buildNavIcon is used to create all three icons for the nav, each one is the same, other than triggering the active state and the icon, so I didn’t want to duplicate code. It uses a Builder to get access to the Navigator while inside of the main app (MyApp), otherwise you would get an error stating that the context found didn’t have a Navigator on it. The Builder returns a Container with the icon for the page, the appropriate background color, and some padding. Wrapping the container is a GestureDetector. The GestureDetector gives us the ability to specify what happens when the user clicks on the container. Normally the nested GestureDetector would never get hit but since we changed the behavior of our parent GestureDetector, instead of blocking the user’s taps, it passes them through to onTap function defined inside of the _buildNavIcon function.

Thanks for reading! I hope it made enough sense to help!

--

--