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.
Copied to your clipboardval surfaces = mutableListOf<Surface>()surfaces.add(Surface("inbox"))Messaging.updatePropositionsForSurfaces(surfaces)
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.
Copied to your clipboardimport com.adobe.marketing.mobile.messaging.MessagingInboxProviderimport com.adobe.marketing.mobile.messaging.Surfaceval surface = Surface("inbox")val inboxProvider = MessagingInboxProvider(surface)
The MessagingInboxProvider provides the following APIs:
getInboxUI()- Returns a Flow ofInboxUIStaterepresenting the current state of the inbox.refresh()- Manually refreshes the inbox content by fetching new propositions.
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:
InboxUIState.Loading- Content is being fetchedInboxUIState.Success- Content was successfully loadedInboxUIState.Error- An error occurred while fetching content
Copied to your clipboardimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport com.adobe.marketing.mobile.aepcomposeui.state.InboxUIStateimport com.adobe.marketing.mobile.messaging.MessagingInboxProviderimport com.adobe.marketing.mobile.messaging.Surfaceimport com.adobe.marketing.mobile.messaging.InboxEventObserverimport kotlinx.coroutines.flow.SharingStartedimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.flow.stateInimport kotlinx.coroutines.launchclass InboxViewModel : ViewModel() {val inboxProvider = MessagingInboxProvider(Surface("inbox"))val observer = InboxEventObserver(inboxProvider)// Convert Flow to StateFlow for easier consumption in Composeval inboxUIState: StateFlow<InboxUIState> = inboxProvider.getInboxUI().stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5_000),initialValue = InboxUIState.Loading)// Function to manually refresh the inboxfun refresh() {viewModelScope.launch {inboxProvider.refresh()}}}
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:
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).
Copied to your clipboardimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.adobe.marketing.mobile.aepcomposeui.components.AepInboximport com.adobe.marketing.mobile.aepcomposeui.style.InboxUIStyle@Composablefun InboxScreen(viewModel: InboxViewModel = viewModel()) {// Collect the inbox state from ViewModelval inboxUIState by viewModel.inboxUIState.collectAsStateWithLifecycle()// Display the inbox with default stylingAepInbox(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:
Copied to your clipboardimport android.os.Bundleimport androidx.activity.viewModelsimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.ui.platform.ComposeViewimport androidx.compose.ui.platform.ViewCompositionStrategyimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport com.adobe.marketing.mobile.aepcomposeui.components.AepInboximport com.adobe.marketing.mobile.aepcomposeui.style.InboxUIStyleclass InboxActivity : AppCompatActivity() {private val viewModel: InboxViewModel by viewModels()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// Create ComposeView programmatically or inflate from XMLval composeView = ComposeView(this).apply {setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)setContent {val inboxUIState = viewModel.inboxUIState.collectAsStateWithLifecycle().valueAepInbox(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:
Copied to your clipboard// In your ViewModelfun refresh() {// Fetch latest content from serverMessaging.updatePropositionsForSurfaces(listOf(surface))// Update the inbox UI with new contentviewModelScope.launch {inboxProvider.refresh()}}// Call from your UIButton(onClick = { viewModel.refresh() }) {Text("Refresh Inbox")}
This is useful for:
- Refreshing on button taps
- Auto-refreshing at intervals
- Refreshing after specific app events
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:
InboxUIState.Loading- Immediately upon collectionInboxUIState.SuccessorInboxUIState.Error- After the fetch completes
Best Practices
Pre-fetch Content: As described in Fetch Inbox settings and Content Cards, call
updatePropositionsForSurfacesearly 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.Use ViewModel: Always manage the
MessagingInboxProviderin a ViewModel to properly handle lifecycle and configuration changes. UseSharingStarted.WhileSubscribedwhen converting the Flow to StateFlow to properly handle configuration changes:
Copied to your clipboardval inboxUIState: StateFlow<InboxUIState> = inboxProvider.getInboxUI().stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = InboxUIState.Loading)
- Wrap in Material Theme: When displaying the inbox, wrap it in your app's Material Theme to ensure proper styling of UI components:
Copied to your clipboardAppTheme {AepInbox(uiState = inboxUIState,inboxStyle = InboxUIStyle.Builder().build(),observer = viewModel.observer)}
- Use Lifecycle-aware Collection: Use
collectAsStateWithLifecycle()instead ofcollectAsState()to automatically stop collecting when the UI is not visible, improving app performance and battery life:
Copied to your clipboardval inboxUIState by viewModel.inboxUIState.collectAsStateWithLifecycle()
- Surface Naming: Use descriptive surface paths that match your Adobe Journey Optimizer campaign configuration. Ensure the same surface string is used consistently between
updatePropositionsForSurfacesandMessagingInboxProvider:
Copied to your clipboard// Use the same surface path in both placesval surface = Surface("inbox")Messaging.updatePropositionsForSurfaces(listOf(surface))val inboxProvider = MessagingInboxProvider(surface)
- Full Refresh Pattern: When implementing a manual refresh (e.g., pull-to-refresh or refresh button), call both
updatePropositionsForSurfacesto fetch fresh content from the server and thenrefresh()to update the UI:
Copied to your clipboardfun onRefreshClicked() {// Fetch latest content from serverMessaging.updatePropositionsForSurfaces(listOf(surface))// Update the inbox UI with new contentviewModelScope.launch {inboxProvider.refresh()}}
Reuse Provider: Keep the
MessagingInboxProviderinstance alive as long as the Inbox view is visible. The provider maintains state and efficiently updates when content changes.Handle Multiple Surfaces: If your app has multiple Inboxes (e.g., notifications, promotions), create separate providers with different surfaces:
Copied to your clipboard// Notifications inboxval notificationsProvider = MessagingInboxProvider(Surface("notifications"))// Promotions inboxval promotionsProvider = MessagingInboxProvider(Surface("promotions"))
