Intro to Android Development
  • Welcome
  • Syllabus
  • Hack Challenge
  • Resources
    • Lecture Videos
    • Ed Discussion
    • Git & GitHub Help/How-To
    • Setting up Android Studio
    • Starting an Android Studio Project & Making an Emulator
    • Importing, Exporting, & Submitting Your Projects to CMS
  • SP25 Course Material
    • Week 1 | Course Logistics, Kotlin, & Basic UI
      • Relevant Links
      • Demo/Lecture: Eatery Card
      • A0: Eatery Card (Follow-Along)
    • Week 2 | States, Components, LazyColumn
      • Relevant Links
      • Demo: Todo List
      • A2: Shopping List
    • Week 3 | Navigation & Animations
      • Relevant Links
      • Demo: Onboarding
      • A3: Stock Trading (RobbingGood)
    • Week 4 | MVVM and Flows
      • Relevant Links
      • Demo: Eatery Card 2
      • A4: Chat of a Lifetime
    • Week 5 | Dumb Components & UIEvents
      • Relevant Links
      • Demo: Music Player
      • A5: Rate My Vibe
    • Week 6 | Coroutines, Networking, JSON
      • Relevant Links
      • Demo: Retrofit
      • A6: You Should Even Lift, Bro.
  • Bonus Week | Android Job Search
    • Relevant Links
    • Android Technical Interview Question!
  • Textbook
    • 1. Introduction to the Editor and Views
      • 1.1 Introduction to the Editor
      • 1.2 SDK Management
      • 1.3 Kotlin Overview
      • 1.4 Views
      • 1.5 Android Studio Project Demo + Understanding The Editor
    • 2. Jetpack Compose
      • 2.1 Introduction
      • 2.2 Layouts
      • 2.3 Modifiers
      • 2.4 Animations
      • 2.5 Lazy Lists
      • 2.6 Reactive UI
    • 3. Intents and Manifest
      • 3.1 Activities
      • 3.2 Implicit Intents
      • 3.3 Explicit Intents
      • 3.4 Manifest
      • 3.5 Permissions
      • 3.6 Summary
    • 4. Navigation
      • 4.1 Types of Navigation
      • 4.2 Implementation of the Bottom Navigation Bar
    • 5. Data and Persistent Storage
      • 5.1 Singleton Classes
      • 5.2 Shared Preferences
      • 5.3 Rooms
      • 5.4 Entities
      • 5.5 Data Access Objects
      • 5.6 Databases
    • 5.5 Concurrency
      • 5.5.1 Coroutines
      • 5.5.2 Implementation of Coroutines
      • 5.5.3 Coroutines with Networking Calls
    • 6. Networking and 3rd Party libraries
      • 6.1 HTTP Overview
      • 6.2 3rd Party Libraries
      • 6.3 JSON and Moshi
      • 6.4 Retrofit
      • 6.5 Summary
    • 7. MVVM Design Pattern
      • 7.1 Key Idea
      • 7.2 Implementation Ideas
    • 8. Flows
    • 9. The Art and Ontology of Software
    • 10. 🔥 Firebase
      • 10.1 Setting up Firebase
      • 10.2 Authentication
      • 10.3 Analytics
      • 10.4 Messaging
      • 10.5 Firestore
  • Additional Topics
    • Git and GitHub
    • Exporting to APK
  • Archive
    • Archived Native Android Textbook Pages
      • 1. Layouts and More Views
        • 1.1 File Structure and File Types
        • 1.2 Resource Files
        • 1.3 Button and Input Control
        • 1.4 ViewGroups
        • 1.5 Summary + A Note On Chapter 2 Topics
      • 2. RecyclerViews
        • 2.1 RecyclerViews
        • 2.2 RecyclerView Performance
        • 2.3 Implementation of a Recycler View
        • 2.4 Implementation with Input Controls
        • 2.5 Filtering RecyclerViews
        • 2.6 Recyclerview Demo
      • 3. ListViews and Searching
        • 3.1 ListView vs. RecyclerView
        • 3.2 ListView Performance
        • 3.3 Implementation of a ListView
        • 3.4 Searching in a List View
      • 4. Fragments
        • 4.1 What are Fragments?
        • 4.2 Lifecycle of a Fragment
        • 4.3 Integrating a Fragment into an Activity
        • 4.4 Sharing Data Between Fragments
        • 4.5 Fragment Slide Shows
      • 5. OkHttp
      • 6. Activity Lifecycle
      • 7. Implementation of Tab Layout
    • Fall 2024 Course Material
      • Lecture 1 & Exercise 1: Introduction to Android
      • Lecture 1.5: Beauty of Kotlin
      • Lecture 2 & HW 2: Modifiers, Lazylists and Reactive UI
      • Lecture 3 & HW 3: Animations, Intents and Manifest
      • Lecture 4 & HW 4: Coroutines & Navigation
      • Lecture 5 & HW 5: Persistent Storage, Networking, and JSON Parsing
      • Lecture 6 & HW 6: MVVM, Flows
      • Bonus Lectures & Bonus HW
      • Bonus Lecture: Industry Practice
    • Spring 2024 Course Material
      • Lecture 1 & Exercise 1: Introduction to Android
      • Lecture 4 & HW 4: LazyLists
      • Lecture 6 & HW 6: Networking, Data, and Persistent Storage
    • Spring 2020 Course Material
      • Week 1: Intro to the Editor
      • Week 2: Views and Layouts
      • Week 3: Intent and Manifest
      • Week 4: ListView and RecyclerView
      • Week 5: Fragments
      • Week 6: Networking
    • Spring 2021 Lecture & HW 8: Networking & 3rd Party APIs
    • HackOurCampus Workshop
Powered by GitBook
On this page
  • Step 0: Dependencies:
  • Step 1: Creating the Screen sealed class
  • Step 2: Initializing a NavHost
  • Step 3: Defining a bottom tab
  • Step 4: Adding the navigation bar
  • Step 5: Future Customization

Was this helpful?

  1. Textbook
  2. 4. Navigation

4.2 Implementation of the Bottom Navigation Bar

The bottom navigation bar is one of the most common type of navigation system in Android apps. Implementing it in Jetpack Compose does require a bit of diligence though. This textbook page should help walk you through the process nicely and explain some of the code in more detail than the demo.

Much of this tutorial can also be applied for basic Compose navigation even if you aren't using a navbar, so if that is your use case, feel free to read on!

Step 0: Dependencies:

To start, you'll need to add the following in your libs.versions.toml file to specify the library versions. Under [libraries] and [plugins], add the corresponding lines:

libs.versions.toml
[libraries]
# ...
androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version="2.8.0-beta06"}
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.6.3" }

[plugins]
# ...
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

Then, in build.gradle file, add the following lines:

build.gradle (Module: app)
plugins {
    // ...
    alias(libs.plugins.jetbrains.kotlin.serialization)
}

dependencies {
    // ...
    implementation(libs.androidx.compose.navigation)
    implementation(libs.kotlinx.serialization.json)
}

Step 1: Creating the Screen sealed class

When using type-safe navigation with compose, it's helpful to have one parent sealed class called Screen that we can use in our app. This way we instantly know which data classes are screens and which ones represent data within our app. Create a sealed class, and create a couple subclasses that extend it with some routes you want your app to have. It could be a good idea to putt this class in its own file.

We will annotate each of these classes with @Serializable, since we want Compose navigation to be able to pass these screen objects between activities, so we need to let the compiler know that these can be converted to strings.

Screen.kt
@Serializable
sealed class Screen {
    @Serializable
    data object HomeScreen : Screen()

    @Serializable
    data object SettingsScreen : Screen()

    @Serializable
    data class ProfileScreen(val userId: String) : Screen()
    // ...
}

Step 2: Initializing a NavHost

The NavHost will select the correct screen to display based on the current route in its navController. It will also specify which route the app starts at. Let's initialize a navController and the corresponding NavHost . You may notice that we are using a Scaffold here. A Scaffold is a layout composable that helps us arrange a bottom bar and main content, so this is only for if you want to use bottom navigation. Also note that we wrapped our content in a Box that uses innerPadding. This is just to make sure that the content is not behind the bottomBar composable.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
         
        setContent {
            FromScratchNavigationTheme {
                val navController = rememberNavController()

                Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = {
                    // TODO we will create our bottom navigation bar here
                }) { innerPadding ->
                    Box(modifier = Modifier.padding(innerPadding)) {
                        NavHost(
                            navController = navController,
                            startDestination = Screen.HomeScreen
                        ) {
                            composable<Screen.HomeScreen> {
                                Text(text = "HOME: add your screen here")
                            }
                            composable<Screen.SettingsScreen> {
                                Text(text = "SETTINGS: add your screen here")
                            }
                            composable<Screen.ProfileScreen> {
                                Text(text = "PROFILE: add your screen here")
                            }
                        }
                    }
                }
            }
        }
    }
}

Step 3: Defining a bottom tab

Each app could have its own notion of a "bottom tab", depending on how you want it to look and what information you need to store. For this demo, I'm going to assume that we want each bottom tab to have an icon, label, and screen. So I am going to represent this with a data class.

data class NavItem(
    val screen: Screen,
    val label: String,
    val icon: ImageVector
)

Then, we need to specify the data of our tabs with a list. This should be held in state in the ViewModel, but for now we can just store the list of tabs in the onCreate method. Here are the tabs I made for this tutorial:

val tabs = listOf(
            NavItem(
                label = "Home",
                icon = Icons.Filled.Home,
                screen = Screen.HomeScreen,
            ),
            NavItem(
                label = "Settings",
                icon = Icons.Filled.Settings,
                screen = Screen.SettingsScreen,
            ),
            NavItem(
                label = "Profile",
                icon = Icons.Filled.Person,
                screen = Screen.ProfileScreen,
            )
        )

Step 4: Adding the navigation bar

To create the navigation bar, we're going to use the bottomBar parameter of the Scaffold layout and pass in a NavigationBar composable. We map our list of tabs to actual composable functions that display the tabs. We can use NavigationBarItem for this. The updated code now looks as follows:

Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = {
            NavigationBar {
                tabs.map { item -> 
                    NavigationBarItem(
                        selected = false,  // TODO
                        onClick = { navController.navigate(item.screen) },
                        icon = { Icon(imageVector = item.icon, contentDescription = null) },
                        label = { Text(text = item.label) }
                    )
                }
            }
        }
    )

However, we still have one problem. How do we know if a NavigationBarItem is selected? For this, we want to use the navBackStackEntry. This is a variable that gives us information about the top of the navigation stack. To use this, we will want to start by creating an extension function in our Screen class that allows us to convert from a nav backstack entry to a Screen.

sealed class Screen {
    // ...
    fun NavBackStackEntry.toScreen(): Screen? =
        when (destination.route?.substringAfterLast(".")?.substringBefore("/")) {
            "HomeScreen" -> toRoute<HomeScreen>()
            "SettingsScreen" -> toRoute<SettingsScreen>()
            "ProfileScreen" -> toRoute<ProfileScreen>()
            else -> null
        }
}

Behind the scenes, a route name might look like "com.example.demo.ui.Screen.Profile/{profileId}", so to get the name of the screen, we look at the substring after the last . . Sometimes routes will have arguments passed to them, so we also have to look before the first / to find the screen name. We then use the toRoute function to automatically parse this route and convert it to its data class representation.

Then we can track the screen as follows:

val navBackStackEntry = navController.currentBackStackEntryAsState().value


Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = {
    NavigationBar {
        tabs.map { item ->
            NavigationBarItem(
                selected = item.screen == navBackStackEntry?.toScreen(),
                onClick = {
                    navController.navigate(item.screen)
                },
                icon = { Icon(imageVector = item.icon, contentDescription = null) },
                label = { Text(text = item.label) }
            )
        }
    }
})

Step 5: Future Customization

Congrats, you implemented a bottom navigation bar in Jetpack Compose! Here are some tips to help you with general navigation skills:

Navigating to other screens

Let's say I wanted to navigate to a ProfileDetails screen from my Profile screen, and I wanted my Profile screen to receive an id.

It's recommended that we pass the navigation action in a lambda to ProfileScreen, instead of giving it access to the entire navController. The reason is, if we put navController as a parameter for ProfileScreen, then that makes ProfileScreen difficult to test. If we wanted to try to test ProfileScreen in isolation, we'd need to provide a mock navigator to it, instead of just being able to provide an empty lambda for navigation to other screens. Not only that, but it makes it harder to reason about the behavior of ProfileScreen , because it can use navController however it wants. So the code looks like this:

NavHost(
    // ... 
) {
    composable<Screen.Home> {
        HomeScreen(navigateToProfileDetails = { id ->
            navController.navigate(Screen.ProfileDetails(id))
        })
    }
    // ...
}

Receiving navigation arguments

To receive navigation arguments on a certain screen, we want to use the NavBackStackEntry. The composable function in NavHost actually takes a parameter of type @Composable() ((NavBackStackEntry) -> Unit), meaning that inside the composable function, the parameter we have access to is the NavBackStackEntry. This is more easily understood through example:

NavHost(
    // ... 
) {
    composable<Screen.ProfileDetails> { navBackStackEntry ->
        val profileId = navBackStackEntry.toRoute<Screen.ProfileDetails>().profileId
        ProfileDetails(profileId)
    }
    // ...
}

Whenever NavHost maps the current navigation destination to a composable, that composable provides navBackStackEntry. We can then get the arguments that we're looking for by using the toRoute function, which takes the back stack entry, and converts it to an instance of our Screen.ProfileDetails data class. We can access the fields of this data class, so that includes profileId. Then we are able to pass this as a parameter to our hypothetical ProfileDetails screen.

Previous4.1 Types of NavigationNext5. Data and Persistent Storage

Last updated 6 months ago

Was this helpful?

We're also going to make ProfileScreen a data class, this will allow us to pass arguments to the screen. We specify a mandatory userId argument here, so whenever someone loads the ProfileScreen, they need to pass a userId. If you want to pass a custom object between screens, I recommend checking out (although it's rare that you would actually need to do this).

this video