When developing any mobile app, it is very important that we think about how the user will move through our app. One of the best ways to help users navigate through an app is to include a Bottom Navigation Bar. Let’s see how we can implement one in Jetpack Compose!

We need the dependency for navigation in this project, as of now I’m using current alpha version.

implementation "androidx.navigation:navigation-compose:2.4.0-alpha10"

1. Create navigation destinations

Let’s compose three different screens as item navigation looks like preview image above.

To add navigation capabilities to an app, we must create different composables that will represent the different destinations in our app.

Create a kotlin file and add below composable functions in one place with data.

NavigationViews.kt :

@Composable
fun ContactsScreen(
    contacts: ArrayList<String> = userData()
) {
    LazyColumn(
        verticalArrangement = Arrangement.spacedBy(12.dp),
        modifier = Modifier.padding(top = 28.dp).fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        items(contacts) { contact ->

            Row(
                modifier = Modifier.fillMaxWidth()
                    .padding(start = 16.dp, end = 12.dp, top = 4.dp, bottom = 4.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Card(
                    modifier = Modifier.clip(CircleShape)
                        .align(Alignment.CenterVertically),
                    backgroundColor = pickRandomColor(),
                ) {
                    Text(
                        text = contact[0].toString(),
                        style = MaterialTheme.typography.h6,
                        modifier = Modifier.padding(12.dp).size(24.dp),
                        textAlign = TextAlign.Center,
                        color = Color.Black
                    )
                }
                Spacer(modifier = Modifier.width(28.dp))
                Text(text = contact, style = MaterialTheme.typography.h6)
            }
        }
    }
}

fun userData() = arrayListOf(
    "Al Pacino", "Robert De Niro", "Tommy Lee Jones", "Jon Voight", "Tim Robbins",
    "Morgan Freeman", "Ray Liotta", "Matthew McConaughey", "Christian Bale", "Joe Pesci",
    "Robert Duvall", "Antonio Banderas", "Bradley Cooper", "Johnny Depp", "Brad Pitt",
    "Emile Hirsch"
)

fun pickRandomColor() = Color(
    arrayListOf(
        0xFFE57373, 0xFFBA68C8, 0xFF9575CD, 0xFFF06292,
        0xFF64B5F6, 0xFF4DD0E1, 0xFFFF8A65,
        0xFFFFD54F, 0xFF81C784, 0xFFFFF176,
    ).random()
)
@Composable
fun RecentContactsScreen(
    recent: ArrayList<String> = recentContacts()
) {
    LazyColumn(
        verticalArrangement = Arrangement.spacedBy(28.dp),
        modifier = Modifier.padding(top = 28.dp).fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        items(recent) { contact ->
            Row(
                modifier = Modifier.fillMaxWidth()
                    .padding(start = 16.dp, end = 12.dp, top = 4.dp, bottom = 4.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Card(
                    modifier = Modifier.clip(CircleShape)
                        .align(Alignment.CenterVertically),
                    backgroundColor = pickRandomColor(),
                ) {
                    Text(
                        text = contact[0].toString(),
                        style = MaterialTheme.typography.subtitle1,
                        modifier = Modifier.padding(8.dp).size(24.dp),
                        textAlign = TextAlign.Center,
                        color = Color.Black
                    )
                }

                Spacer(modifier = Modifier.width(28.dp))

                Column(
                    modifier = Modifier.weight(7f)
                ) {
                    Text(text = contact, style = MaterialTheme.typography.subtitle1)
                    Spacer(modifier = Modifier.height(4.dp))
                    Text(text = pickRandomTimeData(), style = MaterialTheme.typography.subtitle1)
                }

                Icon(
                    painter = painterResource(id = R.drawable.phone),
                    contentDescription = null,
                    modifier = Modifier.size(24.dp).weight(1.5f)
                )
            }
        }
    }
}

fun recentContacts() = arrayListOf(
    "Tommy Lee Jones", "Robert De Niro", "Al Pacino", "Tim Robbins", "Matthew McConaughey",
    "Christian Bale", "Antonio Banderas", "Bradley Cooper", "Emile Hirsch"
)

fun pickRandomTimeData(): String {
    val hours = (0..11).random()
    val minutes = (0..59).random()
    val period = listOf("am", "pm").random()
    val day = listOf("Sun", "Mon", "Tues", "Wed", "Thur", "Fri", "Sat").random()
    return "$hours:$minutes $period   $day"
}
@ExperimentalFoundationApi
@Composable
fun FavoriteContactsScreen(
    contacts: ArrayList<String> = favoriteContacts()
) {
    LazyVerticalGrid(
        cells = GridCells.Fixed(2),
        modifier = Modifier.padding(top = 28.dp),
    ) {
        items(contacts) { contact ->

            Column(
                modifier = Modifier.padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Card(
                    modifier = Modifier.clip(CircleShape)
                        .align(Alignment.CenterHorizontally),
                    backgroundColor = pickRandomColor(),
                ) {
                    Text(
                        text = contact[0].toString(),
                        style = MaterialTheme.typography.h4,
                        modifier = Modifier.padding(12.dp).size(48.dp),
                        textAlign = TextAlign.Center,
                        color = Color.Black
                    )
                }

                Spacer(modifier = Modifier.height(12.dp))

                Text(
                    text = contact,
                    style = MaterialTheme.typography.h6,
                    textAlign = TextAlign.Center
                )
            }
        }
    }
}

fun favoriteContacts() = arrayListOf(
    "Robert De Niro", "Al Pacino", "Tommy Lee Jones", "Tim Robbins", "Matthew McConaughey",
    "Christian Bale", "Antonio Banderas", "Bradley Cooper", "Emile Hirsch"
)

2. Create bottom navigation items

We will add three navigation items to show three screens. Create a sealed class with three objects. Each object holds data and constructor properties for that menu item.

1. route — Is a string that defines path of each destination
2. name — Name for the bottom navigation view item
3. icon — Icon for the bottom navigation view item

Make a list of those three destination objects which returns data, we will make use of this while we build BottomNavigation. Add below code in one file.

BottomNavItems.kt :

sealed class BottomNavItems(
    val route: String,
    val name: String,
    val icon: Int
) {
    object Contacts : BottomNavItems("contacts", "Contacts", R.drawable.contacts)
    object Recent : BottomNavItems("recent", "Recent", R.drawable.recent)
    object Favorites : BottomNavItems("account", "Favorites", R.drawable.favorite)
}

val navItems = listOf(
    BottomNavItems.Contacts,
    BottomNavItems.Recent,
    BottomNavItems.Favorites
)

3. Setup NavHost with destinations

Create a composable function for NavHost in a separate file where you can add all navigation layer code. This behaves as a single host for navigation using NavController. Navigating to a destination is handled by NavController.

NavHost needs two arguments
1. NavHostController to manage navigation within host
2. Default start destination, a string that acts as route

Inside NavHost add composable destinations with route and composed design. All destination screens are added above in first index outline.

@ExperimentalFoundationApi
@Composable
fun NavigationHost(
    navController: NavHostController
) {
    NavHost(
        navController = navController,
        startDestination = BottomNavItems.Contacts.route
    ) {
        composable(BottomNavItems.Favorites.route) {
            FavoriteContactsScreen()
        }

        composable(BottomNavItems.Contacts.route) {
            ContactsScreen()
        }

        composable(BottomNavItems.Recent.route) {
            RecentContactsScreen()
        }
    }
}

4. Setup BottomNavigation

Now let’s setup BottomNavigation and add it inside function BottomNavigationView so we can call this function later.

Below are the properties which I have setup for current BottomNavigation in app.

Label — Optional text label for all items
AlwaysShowLabel — If false, the label will only be shown when this item is selected.
SelectedContentColor — The color of the text label and icon when this item is selected.
UnSelectedContentColor — the color of the text label and icon when this item is not selected.
Selected — whether this item is selected, if yes navigate to that route.
Icon — Icon for this item, typically this will be an drawable resource.
OnClick — The callback to be invoked when this item is selected. On click navigates to route.

@Composable
fun BottomNavigationBar(
    navController: NavController,
    items: List<BottomNavItems> = navItems
) {

    BottomNavigation {

        val navBackStackEntry = navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry.value?.destination

        items.forEach { navItem ->

            BottomNavigationItem(
                label = { Text(navItem.name) },
                alwaysShowLabel = true,
                selectedContentColor = MaterialTheme.colors.secondaryVariant,
                unselectedContentColor = MaterialTheme.colors.primaryVariant,
                selected = currentRoute?.hierarchy?.any {
                    navItem.route == it.route
                } == true,
                icon = {
                    Icon(
                        painterResource(id = navItem.icon),
                        contentDescription = navItem.name,
                    )
                },
                onClick = {
                    navController.navigate(navItem.route) {
                        launchSingleTop = true
                        restoreState = true
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                    }
                }
            )
        }
    }
}

Now construct your layout screen using Scaffold with topBar and bottomBar which contains SearchBar and BottomNavigationBar. Finally setup NavigationHost like below.

BottomNavigationView.kt:

@ExperimentalFoundationApi
@Composable
fun BottomNavigationView() {

    val navController = rememberNavController()

    Scaffold(
        topBar = {
            TopAppBar(
                { SearchBar() },
                backgroundColor = MaterialTheme.colors.background,
                elevation = 0.dp
            )
        }, bottomBar = { BottomNavigationBar(navController) }
    ) {
        NavigationHost(navController)
    }
}

And here’s the missing composable search bar for appBar in Scaffold above.

@Composable
fun SearchBar() {

    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 8.dp, start = 4.dp, end = 16.dp)
            .height(56.dp)
            .background(
                color = MaterialTheme.colors.surface,
                RoundedCornerShape(8.dp)
            )
    ) {

        Icon(
            imageVector = Icons.Default.Search,
            contentDescription = "Search Icon",
            tint = Color.Gray,
            modifier = Modifier.padding(start = 4.dp).weight(1.5f)
        )

        Text(
            text = "Search contacts",
            color = Color.Gray,
            style = MaterialTheme.typography.subtitle1,
            modifier = Modifier.weight(7f).padding(start = 16.dp)
        )

        Icon(
            painterResource(id = R.drawable.ic_mic),
            contentDescription = "Search with speech",
            tint = Color.Gray,
            modifier = Modifier.size(24.dp).weight(1.5f)
        )
    }
}

Let’s dissect the code we have here. First we are passing in a NavController to our composable. We will use this NavController later in our composable function to navigate between our three destinations. Then we create a list from our sealed class that accesses the two objects that represent the two routes to the destinations in our app.

Next, we create a BottomNavigation object where we will use the NavController to get the current state of our NavHost. Then, we will loop through our destinations from our sealed class objects to create BottomNavigationItems for our BottomNavigation object. We can use our NavItem objects to create icons for the navigation items and also we can use the route property of such objects to specify where we should navigate with our NavController.

Now, we must incorporate a composable function to behave as our NavHost for our code to run. The code should look like this:

@ExperimentalAnimationApi
@ExperimentalFoundationApi
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setContent {
            LayoutBasicsTheme {
                BottomNavigationView()
            }
        }
    }
}

Great Job! We’ve successfully implemented a Bottom Navigation View in Jetpack Compose!

5. Resources and source code

If you are interested in seeing the full repository for this project, please click the button below. Also, you can find Google’s official documentation on Bottom Navigation in Jetpack Compose at the following link:

Navigating with Compose | Jetpack Compose | Android Developers

Become Author

We are making content on new declarative UI kit Jetpack Compose, if you are willing to build content join us and we will help you get started.

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.