One of the best ways to help users navigate through an app is to include a Bottom Navigation View. 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 beta version. Also make sure you are using latest material and compose dependencies.

You can take reference of this GitHub Repository if you need while following this article.

implementation "androidx.compose.ui:ui:1.2.0-beta02"
implementation "androidx.navigation:navigation-compose:2.5.1"

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.

ParameterType
routeString – Defines path of each destination item
nameString – Title for the each bottom navigation view item
iconInt – 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.

@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
                        }
                    }
                }
            )
        }
    }
}
AttributeValue
LabelOptional text label for all items
AlwaysShowLabelIf false, the label will only be shown when this item is selected
SelectedContentColorColor of the text label and icon when this item is selected
UnSelectedContentColorColor of the text label and icon when this item is not selected
SelectedWhether this item is selected, if yes navigate to that route
IconIcon for this item, typically this will be an drawable resource
OnClickThe callback to be invoked when this item is selected. On click navigates to route

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 discuss the code we have here. First we are passing in a NavController to our composable function to navigate between three destinations.
Then we create a list from objects in our sealed class, and class properties represent destination title, icon and route of bottom navigation view.

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 finally call the composable function in activity to trigger main entry point of the bottom navigation view.

@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

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.