Skip to content

Tab navigation

Success

To use the TabNavigator you should first import cafe.adriel.voyager:voyager-tab-navigator (see Setup).

Voyager provides a specialized navigator for tabs : the TabNavigator.

The Tab interface, like the Screen, has a Content() composable function, but also requires a TabOptions.

object HomeTab : Tab {

    override val options: TabOptions
        @Composable
        get() {
            val title = stringResource(R.string.home_tab)
            val icon = rememberVectorPainter(Icons.Default.Home)

            return remember {
                TabOptions(
                    index = 0u,
                    title = title,
                    icon = icon
                )
            }
        }

    @Composable
    override fun Content() {
        // ...
    }
}

Info

Since tabs aren’t usually reused, its OK to create them as object.

The TabNavigator unlike the Navigator:

  • Don’t handle back presses, because the tabs are siblings
  • Don’t implements the Stack API, just provides a current property

You can use it with a Scaffold to easily create the UI for your tabs.

setContent {
    TabNavigator(HomeTab) {
        Scaffold(
            content = { 
                CurrentTab() 
            },
            bottomBar = {
                BottomNavigation {
                    TabNavigationItem(HomeTab)
                    TabNavigationItem(FavoritesTab)
                    TabNavigationItem(ProfileTab)
                }
            }
        )
    }
}

Warning

Like theCurrentScreen(), you should use CurrentTab instead of tabNavigator.current.Content(), because it will save the Tab’s subtree for you (see SaveableStateHolder).

Use the LocalTabNavigator to get the current TabNavigator, and current to get and set the current tab.

@Composable
private fun RowScope.TabNavigationItem(tab: Tab) {
    val tabNavigator = LocalTabNavigator.current

    BottomNavigationItem(
        selected = tabNavigator.current == tab,
        onClick = { tabNavigator.current = tab },
        icon = { Icon(painter = tab.icon, contentDescription = tab.title) }
    )
}

Sample

Info

Source code here.

TabNavigator + Nested Navigator

For more complex use cases, when each tab should have its own independent navigation, like the Youtube app, you can combine the TabNavigator with multiple Navigators.

Let’s go back to the Tab navigation example.

setContent {
    TabNavigator(HomeTab) {
        // ...
    }
}

But now, the HomeTab will have it’s own Navigator.

object HomeTab : Screen {

    @Composable
    override fun Content() {
        Navigator(PostListScreen())
    }
}

That way, we can use the LocalNavigator to navigate deeper into HomeTab, or the LocalTabNavigator to switch between tabs.

class PostListScreen : Screen {

    @Composable
    private fun GoToPostDetailsScreenButton(post: Post) {
        val navigator = LocalNavigator.currentOrThrow

        Button(
            onClick = { navigator.push(PostDetailsScreen(post.id)) }
        )
    }

    @Composable
    private fun GoToProfileTabButton() {
        val tabNavigator = LocalTabNavigator.current

        Button(
            onClick = { tabNavigator.current = ProfileTab }
        )
    }
}