Displaying Inbox

This tutorial explains how to fetch and display an Inbox in your Android application.

Pre-requisites

Integrate and register AEPMessaging extension in your app.

Overview

The Inbox is a pre-built UI component that displays content cards in a unified container. Unlike individual content cards, the Inbox automatically manages loading states, error handling, empty states, and card layout based on server-side configuration from Adobe Journey Optimizer.

Fetch Inbox settings and Content Cards

To fetch the inbox settings and content cards for the surfaces configured in Adobe Journey Optimizer campaigns, call the updatePropositionsForSurfaces API. You should batch requests for multiple surfaces in a single API call when possible. The returned inbox settings and content cards are cached in-memory by the Messaging extension and persist through the application's lifecycle.

data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

val surfaces = mutableListOf<Surface>()
surfaces.add(Surface("inbox"))
Messaging.updatePropositionsForSurfaces(surfaces)
data-variant=info
data-slots=text
This API call fetches the inbox and its content from the server and caches it locally. You should call this early in your app lifecycle (e.g., in your Application class or when the user logs in) to ensure content is available when the inbox is displayed.

Create MessagingInboxProvider

To display an Inbox, first create a MessagingInboxProvider with your configured surface. This provider is responsible for fetching inbox content and managing the inbox state through reactive updates.

data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

import com.adobe.marketing.mobile.messaging.MessagingInboxProvider
import com.adobe.marketing.mobile.messaging.Surface

val surface = Surface("inbox")
val inboxProvider = MessagingInboxProvider(surface)

The MessagingInboxProvider provides the following APIs:

data-variant=info
data-slots=text
The Inbox automatically handles layout (vertical/horizontal), styling, and unread indicators based on server-side configuration from Adobe Journey Optimizer campaigns.

Retrieve Inbox State

To retrieve the inbox state, create a MessagingInboxProvider in your ViewModel and call getInboxUI(). This returns a Flow of InboxUIState objects that represent different states of the inbox:

data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.adobe.marketing.mobile.aepcomposeui.state.InboxUIState
import com.adobe.marketing.mobile.messaging.MessagingInboxProvider
import com.adobe.marketing.mobile.messaging.Surface
import com.adobe.marketing.mobile.messaging.InboxEventObserver
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

class InboxViewModel : ViewModel() {
    val inboxProvider = MessagingInboxProvider(Surface("inbox"))

    val observer = InboxEventObserver(inboxProvider)

    // Convert Flow to StateFlow for easier consumption in Compose
    val inboxUIState: StateFlow<InboxUIState> = inboxProvider.getInboxUI()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = InboxUIState.Loading
        )

    // Function to manually refresh the inbox
    fun refresh() {
        viewModelScope.launch {
            inboxProvider.refresh()
        }
    }
}
data-variant=info
data-slots=text
Only content cards for which the user has qualified are returned. Client-side rules are defined in the Adobe Journey Optimizer campaign.

Display Inbox in Compose UI

The Inbox user interface is implemented using Jetpack Compose. To display the inbox, use the AepInbox composable with the InboxUIState from your ViewModel:

data-variant=warning
data-slots=text1, text2
Do not embed AepInbox inside an unbounded container/lazy layout that scrolls in the same direction.
AepInbox uses a LazyColumn for vertical layouts and a LazyRow for horizontal layouts. The orientation is set when the inbox campaign is authored and published on Adobe Journey Optimizer UI. Embedding AepInbox inside an unbounded container/lazy layout that scrolls in the same direction — such as a LazyColumn for a vertical scrolling inbox or a LazyRow for a horizontal scrolling inbox — causes a runtime crash (IllegalStateException: Vertically/Horizontally scrollable component was measured with an infinity maximum constraints).
data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.adobe.marketing.mobile.aepcomposeui.components.AepInbox
import com.adobe.marketing.mobile.aepcomposeui.style.InboxUIStyle

@Composable
fun InboxScreen(viewModel: InboxViewModel = viewModel()) {
    // Collect the inbox state from ViewModel
    val inboxUIState by viewModel.inboxUIState.collectAsStateWithLifecycle()

    // Display the inbox with default styling
    AepInbox(
        uiState = inboxUIState,
        inboxStyle = InboxUIStyle.Builder().build(),
        observer = viewModel.observer
    )
}

Display Inbox in View-based UI

To display an Inbox in a View-based (non-Compose) application, use ComposeView to embed the Compose UI within your Activity or Fragment:

data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.adobe.marketing.mobile.aepcomposeui.components.AepInbox
import com.adobe.marketing.mobile.aepcomposeui.style.InboxUIStyle

class InboxActivity : AppCompatActivity() {

    private val viewModel: InboxViewModel by viewModels()

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

        // Create ComposeView programmatically or inflate from XML
        val composeView = ComposeView(this).apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                val inboxUIState = viewModel.inboxUIState.collectAsStateWithLifecycle().value

                AepInbox(
                    uiState = inboxUIState,
                    inboxStyle = InboxUIStyle.Builder().build(),
                    observer = viewModel.observer
                )
            }
        }

        setContentView(composeView)
    }
}

Refreshing Inbox Data

Inbox provides ways to refresh content cards:

Programmatic Refresh

You can programmatically refresh the Inbox using the refresh() method on the provider:

data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

// In your ViewModel
fun refresh() {
    // Fetch latest content from server
    Messaging.updatePropositionsForSurfaces(listOf(surface))
    // Update the inbox UI with new content
    viewModelScope.launch {
        inboxProvider.refresh()
    }
}

// Call from your UI
Button(onClick = { viewModel.refresh() }) {
    Text("Refresh Inbox")
}

This is useful for:

Automatic Refresh on Initial Load

The getInboxUI() method automatically calls refresh() when first collected, so you don't need to manually trigger the initial load. The flow will emit:

  1. InboxUIState.Loading - Immediately upon collection
  2. InboxUIState.Success or InboxUIState.Error - After the fetch completes

Best Practices

  1. Pre-fetch Content: As described in Fetch Inbox settings and Content Cards, call updatePropositionsForSurfaces early in your app lifecycle (e.g., Application class, splash screen, or after user login) to ensure content is cached and ready when the inbox is displayed.

  2. Use ViewModel: Always manage the MessagingInboxProvider in a ViewModel to properly handle lifecycle and configuration changes. Use SharingStarted.WhileSubscribed when converting the Flow to StateFlow to properly handle configuration changes:

data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

val inboxUIState: StateFlow<InboxUIState> = inboxProvider.getInboxUI()
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = InboxUIState.Loading
    )
  1. Wrap in Material Theme: When displaying the inbox, wrap it in your app's Material Theme to ensure proper styling of UI components:
data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

AppTheme {
    AepInbox(
        uiState = inboxUIState,
        inboxStyle = InboxUIStyle.Builder().build(),
        observer = viewModel.observer
    )
}
  1. Use Lifecycle-aware Collection: Use collectAsStateWithLifecycle() instead of collectAsState() to automatically stop collecting when the UI is not visible, improving app performance and battery life:
data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

val inboxUIState by viewModel.inboxUIState.collectAsStateWithLifecycle()
  1. Surface Naming: Use descriptive surface paths that match your Adobe Journey Optimizer campaign configuration. Ensure the same surface string is used consistently between updatePropositionsForSurfaces and MessagingInboxProvider:
data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

// Use the same surface path in both places
val surface = Surface("inbox")
Messaging.updatePropositionsForSurfaces(listOf(surface))
val inboxProvider = MessagingInboxProvider(surface)
  1. Full Refresh Pattern: When implementing a manual refresh (e.g., pull-to-refresh or refresh button), call both updatePropositionsForSurfaces to fetch fresh content from the server and then refresh() to update the UI:
data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

fun onRefreshClicked() {
    // Fetch latest content from server
    Messaging.updatePropositionsForSurfaces(listOf(surface))
    // Update the inbox UI with new content
    viewModelScope.launch {
        inboxProvider.refresh()
    }
}
  1. Reuse Provider: Keep the MessagingInboxProvider instance alive as long as the Inbox view is visible. The provider maintains state and efficiently updates when content changes.

  2. Handle Multiple Surfaces: If your app has multiple Inboxes (e.g., notifications, promotions), create separate providers with different surfaces:

data-slots=heading, code
data-repeat=1
data-languages=Kotlin

Kotlin

// Notifications inbox
val notificationsProvider = MessagingInboxProvider(Surface("notifications"))

// Promotions inbox
val promotionsProvider = MessagingInboxProvider(Surface("promotions"))