Skip to content

Tabs

A native bottom tab bar elevates a Hotwire Native app to make it feel more like a native app. Under the hood, Hotwire Native drives a standard BottomNavigationView from Material Components. This means all the expected features work out of the box, including:

Add a bottom navigation view

We’ll build on top of the app from the Getting Started guide. Each tab needs its own NavigatorHost in the layout so it maintains its own navigation stack. From there, replace the contents of activity_main.xml with the following:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/navigation_navigator_host"
        android:name="dev.hotwire.navigation.navigator.NavigatorHost"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/bottom_nav"
        app:defaultNavHost="false" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/bridge_components_navigator_host"
        android:name="dev.hotwire.navigation.navigator.NavigatorHost"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/bottom_nav"
        app:defaultNavHost="false" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Define the tabs

Then, define a tab for each NavigatorHost with a tabs variable in MainActivity.kt:

val tabs = listOf(
    HotwireBottomTab(
        title = "Navigation",
        iconResId = R.drawable.ic_tab_navigation,
        configuration = NavigatorConfiguration(
            name = "navigation",
            navigatorHostId = R.id.navigation_navigator_host,
            startLocation = "https://hotwire-native-demo.dev"
        )
    ),

    HotwireBottomTab(
        title = "Bridge Components",
        iconResId = R.drawable.ic_tab_bridge_components,
        configuration = NavigatorConfiguration(
            name = "bridge-components",
            navigatorHostId = R.id.bridge_components_navigator_host,
            startLocation = "https://hotwire-native-demo.dev/components"
        )
    )
)

This creates two tabs, Navigation and Bridge Components. The tab bar’s menu is populated automatically from the tabs, so there’s no need to create a menu resource.

Each HotwireBottomTab requires the following parameters:

Optionally, pass isVisible = false to hide a tab.

Set up the controller

Finally, replace the MainActivity class with the following to wire everything up with a HotwireBottomNavigationController:

class MainActivity : HotwireActivity() {
    private lateinit var bottomNavigationController: HotwireBottomNavigationController

    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<View>(R.id.root).applyDefaultImeWindowInsets()

        bottomNavigationController = HotwireBottomNavigationController(
            activity = this,
            view = findViewById(R.id.bottom_nav)
        )
        bottomNavigationController.load(tabs)
    }

    override fun navigatorConfigurations() = tabs.navigatorConfigurations
}

Every tab’s start location is loaded up front, so switching tabs is instant. Pass a second argument to load() to select a different initial tab, and use setOnTabSelectedListener to be notified when the selected tab changes:

bottomNavigationController.load(tabs, selectedTabIndex = 1)
bottomNavigationController.setOnTabSelectedListener { index, tab ->
    // ...
}

Lazy loading

Each tab performs a web visit for its start location. If loading every tab up front is too expensive, opt into lazy loading by passing lazyLoadTabs = true to the controller. A lazily loaded tab doesn’t visit its start location until the user first selects it. The initially selected tab always loads right away.

HotwireBottomNavigationController(
    activity = this,
    view = findViewById(R.id.bottom_nav),
    lazyLoadTabs = true
)

Next: Path Configuration