This article explains how to perform a full integration of an Android app with Releva, using the Releva Android SDK.
Requirements #
- Minimum SDK: API 24 (Android 7.0); target SDK 36
- Kotlin 2.0 or later, Gradle 8.9 or later
- Firebase Cloud Messaging, for push notifications
Add the SDK to your project #
The SDK is distributed through JitPack. Add the repository in settings.gradle.kts:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") } // add this line
}
}
Then add the dependency in your app module’s build.gradle.kts:
dependencies {
implementation("com.github.Releva-ai:sdk-kotlin:1.7.1")
}
Initialize the Releva Client #
Create the client once, in your Application class, and hold it there — every screen reads it from the same instance. At every launch, set the device id, set the profile id if the user is logged in, and register the current push token:
class MyApplication : Application() {
lateinit var relevaClient: RelevaClient
private set
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
override fun onCreate() {
super.onCreate()
relevaClient = RelevaClient(
context = applicationContext,
realm = "", // usually empty, provided by Releva
accessToken = "<yourAccessToken>", // from the Releva admin panel
config = RelevaConfig.full()
)
applicationScope.launch {
relevaClient.setDeviceId(getDeviceId())
// If the user is logged in, set their id again at every launch:
// relevaClient.setProfileId("<profileId>")
relevaClient.enablePushEngagementTracking()
// Register the current push token at every launch, once the ids are set.
// onNewToken only fires when the token changes, not on an app update.
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
applicationScope.launch {
try {
relevaClient.registerPushToken(DeviceType.ANDROID, token)
} catch (e: Exception) {
Log.e("MyApplication", "Error registering push token", e)
}
}
}
}
}
private fun getDeviceId(): String =
Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
}
Register the class in your AndroidManifest.xml:
<application
android:name=".MyApplication"
... >
Almost every SDK call is a suspend function, so call them from a coroutine scope — lifecycleScope in an Activity or Fragment, viewModelScope in a ViewModel.
Identify the visitor #
The SDK identifies a user by the profileId you set. Until you set one, the visitor is anonymous and identified by the device id — they are still tracked and can still receive push notifications. Send contact details and other profile attributes from your backend through the Create / Update Profile API instead.
suspend fun setProfileId(
profileId: String,
skipMergeWithPreviousProfileId: Boolean = false
)
On login, the default is what you want: if a previous profile id was set on this device, that profile is merged into the user’s account, so their earlier behaviour carries over.
relevaClient.setProfileId(userId)
On logout, do the opposite — assign a fresh anonymous id and skip the merge, so the next person using the device does not inherit the previous user’s profile:
val anonymousProfileId = UUID.randomUUID().toString()
relevaClient.setProfileId(anonymousProfileId, skipMergeWithPreviousProfileId = true)
You do not need to re-register the push token after a login or logout. setProfileId moves the device’s push token to the new profile in the background, so a logged-out user’s notifications stop reaching the device and the new profile’s start to. If that move fails (for example, the user logged in while offline), the next setProfileId or registerPushToken call retries it — which is why both should run at every launch, as in the example above.
Do not use setProfileId("") to log a user out — assign a fresh anonymous id as above.
Push notifications #
Add Firebase #
Follow the standard Firebase Android setup, then add the dependencies and the notification permission Android 13 and later require:
dependencies {
implementation("com.google.firebase:firebase-messaging:23.4.0")
implementation("androidx.core:core-ktx:1.12.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Extend the SDK messaging service #
The SDK ships a Firebase messaging service that displays the notification, tracks engagement and routes the tap. Subclass it and fill in the four app-specific pieces. onPushTokenGenerated runs when Firebase issues a new token; together with the registration at launch shown above, it keeps Releva up to date:
class MyFirebaseMessagingService : RelevaFirebaseMessagingService() {
override fun getMainActivityClass(): Class<*> = MainActivity::class.java
override fun getNotificationIcon(): Int = R.drawable.ic_notification
override fun getDefaultNotificationTitle(): String = "My App"
override fun onPushTokenGenerated(token: String) {
val relevaClient = (application as MyApplication).relevaClient
CoroutineScope(Dispatchers.IO).launch {
try {
relevaClient.registerPushToken(DeviceType.ANDROID, token)
} catch (e: Exception) {
Log.e(TAG, "Error registering push token", e)
}
}
}
}
A token can be registered before the user logs in: an anonymous device is registered under its device id, and the token follows the user to their profile when you call setProfileId.
Register it in your manifest:
<service
android:name=".push.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Handle the navigation #
A notification can carry one of three targets: screen (a named screen in your app, with optional parameters), url (opened in the browser), or nothing at all (your main screen). The SDK decides which one applies and calls your handler for the screen case — implement NavigationHandler and map the names to your own navigation:
class AppNavigationHandler(private val navController: NavController) : NavigationHandler {
override fun navigateToScreen(screenName: String, parameters: Bundle) {
// screenName is the free-form value configured in the Releva dashboard
when (screenName) {
"cart" -> navController.navigate(R.id.cartFragment)
"product_details" -> navController.navigate(R.id.productFragment, parameters)
else -> navController.navigate(R.id.homeFragment)
}
}
}
Register it in your main Activity, and hand the SDK both the launching intent and any later ones:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navController = findNavController(R.id.nav_host_fragment)
NavigationService.getInstance().setNavigationHandler(AppNavigationHandler(navController))
// The app was opened from a notification
NavigationService.getInstance().handleNotificationNavigation(this, intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// The app was already running
NavigationService.getInstance().handleNotificationNavigation(this, intent)
}
}
Miss the onNewIntent override and taps will work from a cold start but do nothing while the app is already open.
Track what the visitor is doing #
Screen views, product views, search, checkout and recommendations all go through one call: build a PushRequest with the fluent builder, then call relevaClient.push(request). The builder methods chain in any order.
lifecycleScope.launch {
try {
val request = PushRequest()
.url("myapp://product/details")
.screenToken("<yourScreenToken>")
.productView(ViewedProduct(productId = productId, custom = CustomFields.empty()))
.pageCategories(listOf("electronics", "phones"))
val response = relevaClient.push(request)
if (response.hasRecommenders) {
renderRecommendationsInYourUI(response.recommenders)
}
} catch (e: Exception) {
Log.e(TAG, "Error tracking product view", e)
}
}
The SDK returns recommendation data; it renders none of it. Building the product cards is your app’s job.
Two fields that are easy to get wrong #
| Field | What it must be |
|---|---|
screenToken | A token from the Pages section of your Releva admin, not a name you invent. "abc123def456" is a token; "product_detail" is not, and a screen sent with one records no page view and returns no banners or stories. Use null until you have configured the screen in Releva. |
url | A full URL, with a scheme — either your own (myapp://product/details) or https://. A bare path such as /product is rejected by the backend. null is fine if you do not want to record one. |
The main builder methods: