Overview
iOS Integration
Android Integration
React Native Integration
Flutter Integration
Use the LowCodeMobile Android SDK to display personalized content in your Android app with minimal setup. The SDK renders out-of-the-box Banner and Recommendations components and supports custom components for fully custom UI.
Add the required Maven repositories to your project-level settings.gradle.kts (or build.gradle):
1repositories {
2 maven {
3 url "https://salesforce-marketingcloud.github.io/MarketingCloudSDK-Android/repository"
4 }
5 maven {
6 url "https://salesforce-marketingcloud.github.io/mobile-sdk-cdp-android/repository"
7 }
8 maven {
9 url "https://salesforce-marketingcloud.github.io/Personalization-Android/repository"
10 }
11}Add the LowCodeMobile dependencies to your app-level build.gradle.kts:
If you are using the out-of-the-box components (the SDK and lowcodemobile dependencies are pulled in automatically):
1dependencies {
2 implementation("com.salesforce.personalization:ootbcomponents:1.0.+")
3}If you are not using the out-of-the-box components (the SDK dependency is pulled in automatically):
1dependencies {
2 implementation("com.salesforce.personalization:lowcodemobile:1.0.+")
3}Your app’s SDK levels must meet or exceed the following minimums.
| Setting | Required |
|---|---|
minSdk | 26 (Android 8.0) |
compileSdk | 34 (Android 14) |
| Kotlin | 2.4+ |
The host app owns SDK initialization. Initialize both the Data 360 module and Personalization modules in your Application.onCreate(). The SDK queues any content zone requests made before initialization completes, so you do not need to gate your UI on init status.
1import com.salesforce.marketingcloud.cdp.CdpConfig
2import com.salesforce.marketingcloud.sfmcsdk.SFMCSdk
3import com.salesforce.marketingcloud.sfmcsdk.SFMCSdkModuleConfig
4import com.salesforce.personalization.PersonalizationConfig
5
6/* Initialization of the CDP module for data collection */
7val flushRate = EventFlushRate.Quantity(quantity = 1)
8
9val cdpConfig = CdpConfig.Builder(
10 context = this,
11 appId = "<Your Data 360 CDP App ID>", // Your CDP Mobile Connector app ID
12 endpoint = "<Your Tenant Specific CDP Endpoint>" // Your CDP Mobile Connector endpoint
13).eventFlushRate(flushRate)
14 .build()
15
16val personalizationConfig = PersonalizationConfig.Builder(this)
17 .dataspace("default") // Optional — defaults to "default" if omitted
18 .cdnUrl("<Your CDN URL>") // Your CDN URL from the Mobile Connector
19 .build()
20
21/* The CDP and Personalization configurations initialized above are provided to SFMCSdk.configure */
22SFMCSdk.configure(this, SFMCSdkModuleConfig.build {
23 cdpModuleConfig = cdpConfig
24 personalizationModuleConfig = personalizationConfig
25})
26{ initStatus ->
27 Log.d(TAG, "SDK initialized: ${initStatus.status}")
28}In your production application, you must explicitly manage user consent using the solution provided in the Engagement Mobile SDK. See Data 360 Consent Management.
Note
"<Your CDP App ID>", "<Your CDP Endpoint>", and "<Your CDN URL>" with the values provided by your Salesforce Marketing Cloud administrator.AndroidManifest.xml <meta-data> entries or a build configuration.SFMCSdk.setLogging(LogLevel.DEBUG, AndroidLogger()) before SFMCSdk.configure(...), so logging is active for the whole init sequence. Remove this before building for production.Once the SDK is initialized, use the ContentZone composable in Jetpack Compose. Pass the personalization point name and a list of components the zone is allowed to render. The SDK fetches a decision from the backend, matches the component name returned, and renders the matching component.
After adding a ContentZone to your app code, create the matching content zone record in Salesforce Personalization. See Set Up Mobile Content Zones for instructions on defining personalization points, assigning components, and configuring engagement definitions.
1import com.salesforce.personalization.lowcodemobile.ContentZone
2import com.salesforce.personalization.ootbcomponents.SalesforceBanner
3import com.salesforce.personalization.ootbcomponents.SalesforceRecommendations
4
5@Composable
6fun HomeScreen() {
7 ContentZone(
8 name = "HomeScreen",
9 allowedComponents = listOf(
10 SalesforceBanner(),
11 SalesforceRecommendations()
12 )
13 )
14}ContentZone is a standard composable - place it anywhere in your layout alongside other content. The Pull-to-refresh example below shows it embedded within a scrollable screen.
The out-of-the-box component names registered internally are "Salesforce_Banner" and "Salesforce_Recommendations".
| Parameter | Required | Description |
|---|---|---|
name | Yes | The personalization point name, matching your backend configuration (case-sensitive). |
allowedComponents | Yes | The Components the zone may render. If the backend returns a name not in this list, the fallback is shown. |
decisionsRequestContext | No | Optional context to bias personalization decisions. |
timeoutMs | No | Fetch timeout in milliseconds. Default: 10,000 (10 seconds). |
controller | No | ContentZoneController for programmatic refresh. |
loading | No | Composable shown while content is loading. |
fallback | No | Composable shown when content cannot be loaded or no component matches. |
Use a ContentZoneController to refresh the zone programmatically:
1import androidx.compose.material3.pulltorefresh.PullToRefreshBox
2import androidx.compose.runtime.remember
3import com.salesforce.personalization.lowcodemobile.ContentZone
4import com.salesforce.personalization.lowcodemobile.ContentZoneController
5import com.salesforce.personalization.ootbcomponents.banner.SalesforceBanner
6import com.salesforce.personalization.ootbcomponents.recs.SalesforceRecommendations
7import kotlinx.coroutines.launch
8
9@Composable
10fun HomeScreen() {
11 val zoneController = remember { ContentZoneController() }
12 var isRefreshing by remember { mutableStateOf(false) }
13 val scope = rememberCoroutineScope()
14
15 PullToRefreshBox(
16 isRefreshing = isRefreshing,
17 onRefresh = {
18 isRefreshing = true
19 scope.launch {
20 try {
21 zoneController.refresh()
22 } finally {
23 isRefreshing = false
24 }
25 }
26 }
27 ) {
28 Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
29 Text("Welcome back!") // Other content in your screen
30
31 ContentZone(
32 name = "HomeScreen",
33 controller = zoneController,
34 allowedComponents = listOf(
35 SalesforceBanner(),
36 SalesforceRecommendations()
37 )
38 )
39
40 // Additional content below the zone
41 }
42 }
43}The SDK ships with two pre-built components - SalesforceBanner and SalesforceRecommendations - that handle layout, styling, and configuration-driven engagement tracking out of the box. Use these when you want to display personalized content without writing a custom UI. Both support an onTap callback for custom tap handling; by default, tapping opens the ctaUrl if one is provided by the backend.
These components are available out-of-the-box in the UI and are registered under the names "Salesforce_Banner" and "Salesforce_Recommendations".
1ContentZone(
2 name = "HomeScreen",
3 allowedComponents = listOf(
4 SalesforceBanner(
5 onTap = { model ->
6 // Custom tap handling - default behavior opens ctaUrl if provided
7 Log.d("App", "Banner tapped: ${model.header}")
8 }
9 )
10 ),
11 loading = { CircularProgressIndicator() },
12 fallback = { Text("Content unavailable") }
13)Pass a DecisionsRequestContext to bias recommendations based on what the user is currently viewing. All fields are optional - if you only supply anchorId, the system infers the type automatically.
1val context = DecisionsRequestContext.Builder()
2 .anchorId("PRODUCT_123")
3 // Optional: explicitly specify the anchor type. If omitted, the system infers it from anchorId.
4 .anchorDmoName("ssot__GoodsProduct__dlm")
5 .contextualAttribute(name = "category", value = "shoes")
6 .build()
7
8ContentZone(
9 name = "ProductRecommendations",
10 allowedComponents = listOf(
11 Recommendations(
12 onTap = { event ->
13 // Custom tap handling - default behavior opens ctaUrl if provided
14 Log.d("App", "Tapped ${event.item.header} at index ${event.index}")
15 }
16 )
17 ),
18 decisionsRequestContext = context
19)Override the default appearance of out-of-the-box components by passing a style object:
1import androidx.compose.ui.graphics.Color
2import androidx.compose.ui.unit.dp
3import com.salesforce.personalization.lowcodemobile.ContentZone
4import com.salesforce.personalization.ootbcomponents.banner.BannerStyle
5import com.salesforce.personalization.ootbcomponents.banner.SalesforceBanner
6
7ContentZone(
8 name = "MyScreen",
9 allowedComponents = listOf(
10 SalesforceBanner(
11 style = BannerStyle(
12 backgroundColor = Color(0xFFF5F5F5),
13 headerTextColor = Color(0xFF1A1A1A),
14 contentPadding = 16.dp
15 )
16 )
17 )
18)Implement the Component<Model> interface (package com.salesforce.personalization.lowcodemobile) to render a content zone with your own UI. Each component defines:
val name: String - The component name that matches the backend experience template’s component name.val modelClass: KClass<Model> - The Kotlin class reference for the component model.validateAndCreateComponentModel(unvalidatedJson, componentContext) - Validates the JSON payload and returns a Result.Success(model) or Result.Failure(exception).@Composable Compose(model, componentContext) - Renders the composable UI for the component.Define a custom component with its own component name, model, and associated experience template in the backend. Give the component a unique name (for example, CustomHero), then create a matching experience template in the Core UI and a matching custom component in your app. Your model’s fields and types must match the experience template’s schema, though you don’t need to use all of them.
1import androidx.compose.foundation.clickable
2import androidx.compose.foundation.layout.Column
3import androidx.compose.foundation.layout.fillMaxWidth
4import androidx.compose.foundation.layout.height
5import androidx.compose.material3.MaterialTheme
6import androidx.compose.material3.Text
7import androidx.compose.runtime.Composable
8import androidx.compose.ui.Modifier
9import androidx.compose.ui.layout.ContentScale
10import androidx.compose.ui.text.style.TextAlign
11import androidx.compose.ui.unit.dp
12import coil.compose.AsyncImage
13import com.salesforce.personalization.errors.PersonalizationException
14import com.salesforce.personalization.lowcodemobile.Component
15import com.salesforce.personalization.lowcodemobile.ComponentContext
16import com.salesforce.personalization.lowcodemobile.ComponentModel
17import com.salesforce.personalization.lowcodemobile.EngagementAction
18import com.salesforce.personalization.lowcodemobile.TrackEngagementViewOnce
19import com.salesforce.personalization.utils.Result
20import kotlinx.serialization.Serializable
21import kotlinx.serialization.json.Json
22
23@Serializable
24data class HeroModel(
25 val header: String,
26 val imageUrl: String,
27 val ctaText: String?
28) : ComponentModel
29
30class CustomHero : Component<HeroModel> {
31 override val name = "CustomHero"
32 override val modelClass = HeroModel::class
33
34 internal companion object {
35 private val json = Json { ignoreUnknownKeys = true }
36 }
37
38 override fun validateAndCreateComponentModel(
39 unvalidatedJson: String,
40 componentContext: ComponentContext
41 ): Result<HeroModel, PersonalizationException.ContentZoneUnableToRender> {
42 val model = json.decodeFromString<HeroModel>(unvalidatedJson)
43 // Optionally, validate model (blank strings, no fallbacks, missing critical pieces) and if invalid:
44 // return Result.Failure(PersonalizationException.ComponentDataModelInvalid(...))
45 return Result.Success(model)
46 }
47
48 @Composable
49 override fun Compose(model: HeroModel, componentContext: ComponentContext) {
50 Column(
51 modifier = Modifier
52 .fillMaxWidth()
53 .clickable {
54 // Track clicks when the user taps
55 componentContext.trackEngagement(EngagementAction.CLICK)
56 // You could add a tapUrl to the model and open/route as well
57 }
58 ) {
59 // This example uses AsyncImage from the Coil dependency
60 AsyncImage(
61 model = model.imageUrl,
62 contentDescription = model.header,
63 modifier = Modifier.fillMaxWidth().height(200.dp),
64 contentScale = ContentScale.Crop
65 )
66 Text(
67 text = model.header,
68 style = MaterialTheme.typography.titleMedium,
69 modifier = Modifier.fillMaxWidth(),
70 textAlign = TextAlign.Center
71 )
72 model.ctaText?.let {
73 Text(
74 text = it,
75 modifier = Modifier.fillMaxWidth(),
76 color = MaterialTheme.colorScheme.primary,
77 textAlign = TextAlign.Center
78 )
79 }
80 }
81 // Track a view once per personalization
82 TrackEngagementViewOnce(componentContext)
83 }
84}
85
86// Usage
87ContentZone(
88 name = "HomeScreen",
89 allowedComponents = listOf(CustomHero())
90)For a component that renders a list (like SalesforceRecommendations), use the per-item variants of the engagement functions to report engagement for each item in the list.
1import androidx.compose.foundation.clickable
2import androidx.compose.foundation.layout.Arrangement
3import androidx.compose.foundation.layout.Column
4import androidx.compose.foundation.layout.Row
5import androidx.compose.foundation.layout.Spacer
6import androidx.compose.foundation.layout.fillMaxWidth
7import androidx.compose.foundation.layout.height
8import androidx.compose.foundation.layout.size
9import androidx.compose.material3.MaterialTheme
10import androidx.compose.material3.Text
11import androidx.compose.runtime.Composable
12import androidx.compose.ui.Alignment
13import androidx.compose.ui.Modifier
14import androidx.compose.ui.layout.ContentScale
15import androidx.compose.ui.text.font.FontWeight
16import androidx.compose.ui.text.style.TextAlign
17import androidx.compose.ui.text.style.TextOverflow
18import androidx.compose.ui.unit.dp
19import coil.compose.AsyncImage
20import com.salesforce.personalization.errors.PersonalizationException
21import com.salesforce.personalization.lowcodemobile.Component
22import com.salesforce.personalization.lowcodemobile.ComponentContext
23import com.salesforce.personalization.lowcodemobile.ComponentModel
24import com.salesforce.personalization.lowcodemobile.EngagementAction
25import com.salesforce.personalization.lowcodemobile.TrackEngagementViewOncePerItem
26import com.salesforce.personalization.utils.Result
27import kotlinx.serialization.Serializable
28import kotlinx.serialization.json.Json
29
30@Serializable
31data class ItemListModel(
32 val sectionHeader: String?,
33 val items: List<ItemModel>
34) : ComponentModel
35
36@Serializable
37data class ItemModel(
38 val id: String,
39 val title: String,
40 val imageUrl: String,
41 val description: String
42)
43
44class CustomItemList : Component<ItemListModel> {
45 override val name = "CustomItemList"
46 override val modelClass = ItemListModel::class
47
48 internal companion object {
49 private val json = Json { ignoreUnknownKeys = true }
50 }
51
52 override fun validateAndCreateComponentModel(
53 unvalidatedJson: String,
54 componentContext: ComponentContext
55 ): Result<ItemListModel, PersonalizationException.ContentZoneUnableToRender> {
56 val model = json.decodeFromString<ItemListModel>(unvalidatedJson)
57 // Optionally, validate model (blank strings, no fallbacks, missing critical pieces) and if invalid:
58 // return Result.Failure(PersonalizationException.ComponentDataModelInvalid(...))
59 return Result.Success(model)
60 }
61
62 @Composable
63 override fun Compose(model: ItemListModel, componentContext: ComponentContext) {
64 Column(
65 modifier = Modifier.fillMaxWidth(),
66 verticalArrangement = Arrangement.spacedBy(16.dp)
67 ) {
68 model.sectionHeader?.takeIf { it.isNotBlank() }?.let { header ->
69 Text(
70 text = header,
71 style = MaterialTheme.typography.titleMedium,
72 modifier = Modifier.fillMaxWidth(),
73 textAlign = TextAlign.Center
74 )
75 }
76 model.items.forEachIndexed { index, item ->
77 ComposeItem(item, componentContext, index)
78 }
79 }
80 }
81}
82
83@Composable
84private fun ComposeItem(model: ItemModel, componentContext: ComponentContext, index: Int) {
85 Row(
86 verticalAlignment = Alignment.CenterVertically,
87 modifier = Modifier.clickable {
88 // Track clicks when the user taps
89 componentContext.trackEngagementPerItem(index, EngagementAction.CLICK)
90 // You could add a tapUrl to the model and open/route as well
91 }
92 ) {
93 // Image on the left. This example uses AsyncImage from the Coil dependency.
94 AsyncImage(
95 model = model.imageUrl,
96 contentDescription = model.title,
97 modifier = Modifier.size(100.dp),
98 contentScale = ContentScale.Crop
99 )
100 // Text on the right
101 Column(
102 modifier = Modifier.weight(1f),
103 verticalArrangement = Arrangement.Center
104 ) {
105 Text(
106 text = model.title,
107 fontWeight = FontWeight.SemiBold,
108 maxLines = 1,
109 overflow = TextOverflow.Ellipsis
110 )
111 Spacer(modifier = Modifier.height(8.dp))
112 Text(
113 text = model.description,
114 fontWeight = FontWeight.Normal,
115 maxLines = 1,
116 overflow = TextOverflow.Ellipsis
117 )
118 }
119 }
120 // Track a view once per personalization, per item
121 TrackEngagementViewOncePerItem(componentContext, index)
122}
123
124// Usage
125ContentZone(
126 name = "HomeScreen",
127 allowedComponents = listOf(CustomItemList())
128)Before engagement events are recorded, ensure your engagement definitions (View, Click) are configured in the Data 360 mobile connector. See Set Up Mobile Engagement Tracking for instructions on defining engagement actions alongside your components and content zones.
Out-of-the-box components (SalesforceBanner, SalesforceRecommendations) track View and Click engagement automatically. These are the only two actions currently supported end-to-end. The SDK accepts custom action strings for future expansion.
For custom components, engagement is not automatic - you must call these methods explicitly from your component code:
| Function | Use for |
|---|---|
componentContext.trackEngagement(action) | Clicks on a single-item component (for example, CustomHero) |
componentContext.trackEngagementPerItem(index, action) | Clicks on an item within a multi-item component (for example, CustomItemList) |
TrackEngagementViewOnce(componentContext) | Views of a single-item component - a Composable, call once per personalization |
TrackEngagementViewOncePerItem(componentContext, index) | Views of an item within a multi-item component - a Composable, call once per item per personalization |
See Define a custom component and Define a multi-item custom component above for both in use.
The SDK supports previewing personalized content via QR code or preview URL. When a URL containing the sfp-preview parameter is opened, the SDK renders preview content in all active content zones.
Before using preview, make sure:
AndroidManifest.xml intent filters).1class MainActivity : ComponentActivity() {
2 override fun onCreate(savedInstanceState: Bundle?) {
3 super.onCreate(savedInstanceState)
4 PersonalizationSdk.handlePreviewIntent(intent)
5 }
6
7 override fun onNewIntent(intent: Intent) {
8 super.onNewIntent(intent)
9 PersonalizationSdk.handlePreviewIntent(intent)
10 }
11}Use MockDataContentZone to render components with mock data for styling and layout without any backend setup or networking. This is not related to QR-code based previewing - it simply lets you see how components look during development.
1@Preview // Compose design-time rendering - not related to QR-code based previewing
2@Composable
3fun HomeScreenPreview() {
4 MockDataContentZone(
5 name = "HomeScreen",
6 allowedComponents = listOf(SalesforceBanner(), SalesforceRecommendations()),
7 mockContent = Result.Success(myMockComponentModel) // replace myMockComponentModel with the model you need, for example:
8 // BannerModel(
9 // header = "Hello",
10 // imageUrl = "https://example.com/banner.png",
11 // ctaText = "Find out more"
12 // )
13 )
14}To try out your fallback composable, pass a Result.Failure:
1@Preview
2@Composable
3fun ErrorStatePreview() {
4 MockDataContentZone(
5 name = "HomeScreen",
6 allowedComponents = listOf(SalesforceBanner()),
7 mockContent = Result.Failure(
8 PersonalizationException(PersonalizationException.ContentZoneUnableToRender("Preview Error"))
9 )
10 )
11}The SDK uses PersonalizationException with a Type enum for error reporting:
| Type | Description |
|---|---|
UNKNOWN | An unknown error occurred. |
INITIALIZATION | The SDK has not been initialized or failed to initialize. |
CONSENT | User consent is not set to opt-in. |
REQUEST_INVALID | The request parameters are invalid. |
NETWORK | A network error occurred during the fetch. |
RESPONSE_INVALID | The server response could not be parsed. |
TIMEOUT | The fetch request exceeded the timeout duration. |
ContentZone handles errors internally. If no fallback is provided, the zone logs the error and shows nothing - this is the typical production behavior. If you want to display fallback content, pass a fallback composable. The following example is for illustration only - consider what behavior is appropriate for your production app:
1ContentZone(
2 name = "HomeScreen",
3 allowedComponents = listOf(SalesforceBanner()),
4 fallback = { error ->
5 when ((error as? PersonalizationException)?.type) {
6 PersonalizationException.Type.TIMEOUT ->
7 Text("Request timed out. Pull to refresh.")
8 PersonalizationException.Type.NETWORK ->
9 Text("No network connection.")
10 else ->
11 Text("Content unavailable.")
12 }
13 }
14)