Android Setup

Hub

Requires SDK 4.13.0 or later

The Hub (previously known as the Communication Hub) is an embeddable app area that includes an inbox of posts and other features. It is intended to be embedded within a tab bar item in the app, and includes support for badging the tab bar item when new content is available.

Hub

This feature is distinct from the legacy "Inbox" feature. The legacy Inbox (InboxActivity et al) is deprecated and will be removed in a future release.

Configuration Change

The title and accentColor parameters were previously documented here. These are now configured server-side, so remove them from your client code.


Jetpack Compose

To embed the Hub composable as a new tab in a typical Jetpack Compose Navigation Bar, use a NavigationBarItem:

import io.rover.sdk.notifications.communicationhub.ui.Hub

enum class SelectedTab {
    /* ... other tab definitions ... */
    RoverHub,
}

@Composable
fun MyApp() {
    var selectedTab: SelectedTab by remember { mutableStateOf(SelectedTab.RoverHub) }

    MaterialTheme(
        // ... ensure a Material 3 theme is configured in your app ...
    ) {
        Scaffold(
            modifier = Modifier.fillMaxSize(),
            bottomBar = {
                NavigationBar() {
                    /* ... other tab button items ... */
                    NavigationBarItem(
                        selected = selectedTab == SelectedTab.RoverHub,
                        icon = {
                            // observe the Rover badge count
                            val badgeText by Rover.shared.roverBadge.newBadge.collectAsState()

                            // use a BadgedBox to display the badge if it is non-null
                            BadgedBox(
                                badge = {
                                    badgeText?.let { text ->
                                        Badge { Text(text) }
                                    }
                                }
                            ) {
                                // Use an "Inbox" filled icon from Google's Material Icons. You will need to do the standard Google procedure for adding the Material icon to your app. https://fonts.google.com/
                                Icon(painter = painterResource(id = R.drawable.inboxfilled48), contentDescription = "For You", modifier = Modifier.size(24.dp))
                            }
                        },
                        label = {
                            Text("For You", maxLines = 1)
                        },
                        onClick = {
                            selectedTab = SelectedTab.RoverHub
                        }
                    )
                }
            }
        ) {
            when (selectedTab) {
                /* ... other tab content ... */
                SelectedTab.RoverHub -> {
                    Hub(
                        // any custom jetpack compose modifiers:
                        modifier = Modifier
                    )
                }
        }
    }
}

Theme configuration for WebView in Dark Mode

Even though the Rover Hub uses Jetpack Compose, Android WebView will only honour dark mode if you have a values-night theme (as used by classic Android views) configured with the isLight property set to false. If you don't do this, posts in the Hub will not be displayed in dark mode.

Ensure you have a values-night/themes.xml file in your app with the following content:

<item name="android:isLightTheme">false</item>

App Icon Badging

Unlike iOS, Android does not support automatic app icon badging due to platform limitations and the way Android app badges work. The badge count functionality shown in the examples above only applies to in-app UI elements like tab bars and navigation items, not the app icon itself.

Compose Theme & Accent Color Configuration

When embedded in a tab, the Hub composable will adopt whatever Material 3 theme it is wrapped in (particularly for tinting controls like hyperlinks with the primary/accent color), just as other composables do. By wrapping a Material 3 Jetpack Compose theme around your Compose UI content, this will happen automatically for an embedded Hub.

However, when the Hub is launched from a deep link, this theme provided by UI context is not available. Therefore, in order for the Hub UI to be properly styled, Material 3 color schemes need to be supplied globally to Rover for it to use when it launches a standalone activity with the Hub composable.

Typically, as part of your Material 3 Theme, you will have defined a lightScheme and darkScheme in your app.

You can set these to Rover's global lightColorScheme and darkColorScheme properties to ensure that the Hub is presented in the correct theme when launched from a deep link:

Rover.shared.lightColorScheme = lightScheme
Rover.shared.darkColorScheme = darkScheme

Classic Android Views

To use the Hub in a classic Android Views app with an MDC BottomNavigationView, and display a badge count on the Hub tab, perform the following steps.

  1. Create a Fragment to host the Hub composable:
import io.rover.sdk.notifications.communicationhub.ui.Hub

class HubFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Use ComposeView to host the Hub composable
        return ComposeView(requireContext()).apply {
            setContent {
                // configure a Material 3 theme as you would normally do here. Rover's Hub will adopt it, particularly for the primary (or accent) color.
                MaterialTheme {
                    Hub(
                        // any custom jetpack compose modifiers:
                        modifier = Modifier
                    )
                }
            }
        }
    }
}
  1. In your Activity, set up the BottomNavigationView and observe the badge count:
class MainActivity : AppCompatActivity() {

    private var badgeJob: Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main) // Your layout with a FrameLayout and BottomNavigationView

        // Set initial fragment
        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
            // where "HomeFragment" is the view for your default tab
                .replace(R.id.fragment_container, HomeFragment())
                .commit()
        }

        val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation)

        // Set up badge for Hub tab
        val hubMenuItemId = R.id.tab_hub

        // Observe badge count using a coroutine
        badgeJob = lifecycleScope.launch {
            // Import kotlinx.coroutines.flow.collectLatest
            io.rover.sdk.core.Rover.shared.roverBadge.newBadge.collect { badgeText ->
                val badge = bottomNav.getOrCreateBadge(hubMenuItemId)
                if (badgeText.isNullOrEmpty() || badgeText == "0") {
                    // Remove badge if no unread
                    bottomNav.removeBadge(hubMenuItemId)
                } else {
                    badge.isVisible = true
                    badge.number = badgeText.toIntOrNull() ?: 0
                }
            }
        }

        bottomNav.setOnItemSelectedListener { item ->
            when (item.itemId) {
                /* ... other tab views ... */
                R.id.tab_hub -> {
                    supportFragmentManager.beginTransaction()
                        .replace(R.id.fragment_container, HubFragment())
                        .commit()
                    true
                }
                // ... handle other tabs ...
                else -> false
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        badgeJob?.cancel()
    }
}
  1. In your res/layout/activity_main.xml, you should have something like:
<LinearLayout ... >
    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        ... />
</LinearLayout>
  1. In your res/menu/bottom_nav_menu.xml, define the menu items:
<menu>
    <item
        android:id="@+id/tab_home"
        android:icon="@drawable/ic_home"
        android:title="Home"/>
    <item
        android:id="@+id/tab_hub"
        android:icon="@drawable/ic_inbox"
        android:title="For You"/>
    <!-- ... other tabs ... -->
</menu>

This approach allows you to use the Hub composable in a classic Android Views app with a BottomNavigationView, and display a badge count on the Hub tab that updates automatically.


When a user taps a push notification for a post or a conversation, or opens a Rover deep link such as rv-myapp://posts/{id}, the SDK needs to get the Hub on screen and then navigate to that post. The SDK cannot select your Hub tab or drive your app's navigation, so it asks your app to do that half by opening a deep link that your app owns. This is the Hub deep link, and it is configured in the Rover dashboard under Settings → Channels → Mobile → App HubDeep link. The same value is used for both iOS and Android. The SDK only uses it while Enable Inbox is also switched on for the App Hub.

Once the Hub deep link is set, the flow is:

  1. The SDK records the destination (the post or conversation) in the Hub's own navigation state.
  2. The SDK calls startActivity with an ACTION_VIEW intent for the Hub deep link, restricted to your app's package.
  3. Your activity receives the intent and navigates to wherever it shows the Hub composable.
  4. As soon as the Hub composable is on screen, it reads the destination back and navigates to the post or conversation.

If the Hub deep link is not set, the SDK skips this handoff and shows the post or conversation in its own standalone activity. That works, but the user is not left in the Hub afterwards.

To support the Hub deep link, your app has to do two things.

Pick a URL with your app's own custom scheme, for example myapp://hub, and give it to whoever administers your Rover dashboard so it can be entered in the App Hub Deep link field.

Then add an intent filter for it to the activity that hosts (or can navigate to) your Hub tab. This must be one of your activities. Do not add it to TransientLinkLaunchActivity or any other Rover activity: those handle links the SDK opens, and cannot navigate your UI.

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">

    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!-- The Hub deep link set on the Rover dashboard -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data android:scheme="myapp" android:host="hub" />
    </intent-filter>
</activity>

android.intent.category.DEFAULT is required for Android to resolve the link to your activity at all. Android requires an explicit android:exported value on any activity with an intent filter; use true so the link can also be opened from outside your app, for example when testing. android:launchMode="singleTask" makes Android deliver the link to your running activity through onNewIntent instead of starting a second copy of it.

2. Handle the intent by navigating to the Hub

Handle the link in both onCreate, for when the app is cold started, and onNewIntent, for when it is already running. You do not need to pass the intent to the SDK, the Hub composable picks up the destination on its own when it appears.

Unlike a Rover link (rv-myapp://posts/…), which Android delivers to TransientLinkLaunchActivity and the SDK routes itself, the Hub deep link is the intent the SDK sends to you as a result of that routing. A notification tap never reaches your activity directly; only the follow-up myapp://hub intent does.

Jetpack Compose

Extending the tab example above, hoist selectedTab out of MyApp into the activity so both entry points can set it:

class MainActivity : ComponentActivity() {
    private var selectedTab by mutableStateOf(SelectedTab.Home)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        handleHubDeepLink(intent)

        setContent {
            MyApp(
                selectedTab = selectedTab,
                onTabSelected = { selectedTab = it },
            )
        }
    }

    // `MyApp` from the example above, with its `remember { mutableStateOf(...) }`
    // replaced by these two parameters. Use `onTabSelected(SelectedTab.RoverHub)`
    // in the NavigationBarItem's onClick.
    @Composable
    private fun MyApp(
        selectedTab: SelectedTab,
        onTabSelected: (SelectedTab) -> Unit,
    ) {
        /* ... Scaffold, NavigationBar and tab content as above ... */
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        handleHubDeepLink(intent)
    }

    private fun handleHubDeepLink(intent: Intent?) {
        val uri = intent?.data ?: return
        if (uri.scheme == "myapp" && uri.host == "hub") {
            // Dismiss anything covering the tab bar first, or the Hub opens underneath it.
            selectedTab = SelectedTab.RoverHub
        }
    }
}

Classic Android Views

Select the Hub item on your BottomNavigationView; its selection listener then shows HubFragment as in the example above:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // ... existing setup ...
    handleHubDeepLink(intent)
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    setIntent(intent)
    handleHubDeepLink(intent)
}

private fun handleHubDeepLink(intent: Intent?) {
    val uri = intent?.data ?: return
    if (uri.scheme == "myapp" && uri.host == "hub") {
        findViewById<BottomNavigationView>(R.id.bottom_navigation).selectedItemId = R.id.tab_hub
    }
}

Fire the URL at a device or emulator with the app installed and check that your app brings the Hub forward:

adb shell am start -a android.intent.action.VIEW -d "myapp://hub"

Then, with the Hub deep link entered in the dashboard, send yourself a test post from the Rover dashboard, tap the notification, and confirm the app lands inside the Hub on that post rather than in a standalone activity.

Test both halves before shipping

The SDK relies on your app being able to open the Hub deep link and navigate to the Hub when it arrives, and it does not report a problem with either. If no activity in your app matches the link, the notification tap fails and the app closes instead of showing the post. If the link opens your app but your handler does not navigate to the Hub, the post stays queued until the user next opens the Hub on their own. Check both with the test above.

Previous
Push Notifications