Userpilot Android SDK enables you to capture user insights and deliver personalized in-app experiences in real time. With just a one-time setup, you can immediately begin leveraging Userpilot’s analytics and engagement features to understand user behaviors and guide their journeys in-app. This document provides a step-by-step walkthrough of the installation and initialization process, as well as instructions on using the SDK’s public APIs.

Prerequisites

Before you begin, ensure your Android project meets the following requirements:
  • Android Gradle Plugin: 8.0 or higher.
  • minSdkVersion: 21 or higher.
  • compileSdkVersion: 34 or higher.
  • Kotlin Compatibility: Apply the kotlin-android plugin in your app module’s build.gradle or ensure your Android Gradle Plugin is on 8.4.0 or higher.
Due to the SDK usage of Jetpack Compose, it is required to either:
  1. apply kotlin-android  plugin in app’s build.gradle file.
plugins {  
  id 'com.android.application' 
  id 'kotlin-android' 
}
  1. Or Update Android Gradle Plugin 8.4.0+
Related Google issue regarding usage of the Jetpack Compose dependency versions 1.6+.
Android Gradle Plugin:
If you are using AGP 8.8.0+, the Android Gradle Plugin will automatically use Build Tools version 35.0.0, regardless of the value you specify.
To ensure full compatibility with our SDK, please upgrade your app’s target and compile SDK version to 35.

Installation

The library is distributed through Maven Central. Add the Userpilot module to your build.gradle as a dependency as shown in the code sample below, and replace the <latest_version> with the latest release version).
repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.userpilot:userpilot-android:<latest_version>'
}
Once synced, the Userpilot SDK is available to import throughout your application.

Initialization

To use Userpilot, initialize it once in your Application class. This ensures the SDK is ready as soon as your app starts. Update your Application class. Replace <APP_TOKEN> with your Application Token, which can be fetched from your Environments Page.
class YourApplication : Application() {
    lateinit var userpilot: Userpilot

    override fun onCreate() {
        super.onCreate()
        userpilot = Userpilot(this, "<APP_TOKEN>") {
            loggingEnabled = true // Enable or disable logging
		}
    }
}

Note for Apps Using AndroidX Startup

If your application **also uses **androidx.startup.InitializationProvider, you should not set tools:node="ignore" on this provider to disable it. Doing so will prevent Userpilot SDK from being initialized correctly. Instead, make sure to merge the provider declarations using:tools:node="merge" This ensures that your custom initializers and the Userpilot initializer both get registered properly.

Using the SDK

Once initialized, the SDK provides straightforward APIs for identifying users, tracking events, and screen views.

Identifying Users (Required)

This API is required to identify unique users and companies (groups of users) alongside their properties. Once identified, all subsequent tracked events and screens will be attributed to that user. Recommended Usage:
  • On user authentication (login): Immediately call identify when a user signs in to establish their identity for all future events.
  • On app launch for authenticated users: If the user has a valid authenticated session, call identify at app launch.
  • Upon property updates: Whenever user or company properties change.
userpilot.identify(
    userId = "<USER_ID>",
    properties = mapOf("name" to "John Doe", "email" to "user@example.com", "created_at" to "2019-10-17", "role" to "Admin"),
    company = mapOf("id" to "<COMPANY_ID>", "name" to "Acme Labs", "created_at" to "2019-10-17", "plan" to "Free")
)
Properties Guidelines
  • Key id is required in company properties, to identify a unique company.
  • Userpilot supports String, Numeric, and Date types.
  • Make sure you’re sending date values in ISO8601 format.
  • If you are planning to use Userpilot’s localization features, make sure you are passing user property locale_code with a value that adheres to ISO 639-1 format.
  • Userpilot’s reserved properties’ have pre-determined types and improve profiles interface in the dashboard:
    • Use key email to pass the user’s email.
    • Use key name to pass the user’s or company’s name.
    • Use key created_at to pass the user’s or company’s signed up date.
Notes
  • Make sure your User ID source is consistent across all of your platform installations (Web, Android, and iOS).
  • While properties are optional, they are essential in Userpilot’s segmentation capabilities. We encourage you to set the properties with the people who are responsible for Userpilot integration

Tracking Screens (Required)

Calling screen is crucial for unlocking Userpilot’s core engagement and analytics capabilities. When a user navigates to a particular screen, invoking screen records that view and triggers any eligible in-app experiences. Subsequent events are also attributed to the most recently tracked screen, providing context for richer analytical insights. For these reasons, we strongly recommend tracking all of your app’s screen views.
userpilot.screen("Profile")

Tracking Events

Log any meaningful action the user performs. Events can be button clicks, form submissions, or any custom activity you want to analyze. Optionally, you can pass metadata with the event to provide specific context.
userpilot.track("Added to Cart", mapOf("itemId" to "sku_456", "price" to 29.99))

Logging Out

When a user logs out, call logout() to clear the current user context. This ensures subsequent events are no longer associated with the previous user.
userpilot.logout()

Anonymous Users

If a user is not authenticated, call anonymous() to track events without a user ID. This is useful for pre-signup flows or guest user sessions.
userpilot.anonymous()
Anonymous users are counted towards your Monthly Active Users usage. You should take your account’s MAU limit into consideration before applying this API.

Experience

Triggers a specific experience programmatically using it’s ID. This API allows you to manually initiate an experience within your application.
userpilot.triggerExperience("<EXPERIENCE_ID>")
To end current active experience
userpilot.endExperience()

Configurations (Optional)

If you have additional configuration needs, you can pass a custom configuration to Userpilot’s constructor. Userpilot provides interfaces for implementing listeners or handlers to Userpilot events. Implement these interfaces and pass them in your config on initialization if you need to.
val userpilot = Userpilot(context, "<APP_TOKEN>") {
            loggingEnabled = true // Enable or disable logging
            useInAppBrowser = true
            navigationHandler = object : UserpilotNavigationHandler {
                override fun navigateTo(uri: Uri) {

                }
            }
            analyticsListener = object : UserpilotAnalyticsListener {
                override fun didTrack(
                    type: UserpilotAnalytic,
                    value: String,
                    properties: Payload,
                ) {

                }
            }
            experienceListener = object : UserpilotExperienceListener {
                override fun onExperienceStateChanged(
                    id: Int,
                    state: UserpilotExperienceState
                ) {

                }

                override fun onExperienceStepStateChanged(
                    id: Int,
                    state: UserpilotExperienceState,
                    experienceId: Int,
                    step: Int,
                    totalSteps: Int,
                ) {

                }
            }
        }
Defines how your app handles deep link route triggers configured in Userpilot experiences, requiring you to route the user to the appropriate location.
public interface UserpilotNavigationHandler {
     public fun navigateTo(uri: Uri)
}
The UserPilot SDK automatically handles navigation if you haven’t implemented the UserPilotNavigationHandler . When a deep link with a defined schema and host is present in the manifest file, the SDK will open the corresponding activity. If the link is external, the SDK will handle it appropriately. For complete control over link handling, you can override the UserPilotNavigationHandler interface. This allows you to customize the behavior for all types of links as per your requirements. The useInAppBrowserflag which work when you didn’t implement UserpilotNavigationHandler determines whether to open URLs using an in-app browser (e.g., Chrome Custom Tabs) or fall back to the system browser or a different method.
  • **When **useInAppBrowser**is ** true:
    The application will open the given URL using Chrome Custom Tabs, providing a faster, more integrated experience with features like shared cookies, toolbar customization, and seamless return to the app.
  • **When **useInAppBrowser**is ** falseor not set :
    The app may open the URL using the default system browser or another fallback approach.

Analytics Listener

Receives callbacks whenever the SDK tracks an event, screen, or identifies a user. Use this if you want to integrate with another analytics tool.
public interface UserpilotAnalyticsListener {

    /**
     * Notifies the listener when a Userpilot analytics event is tracked.
     *
     * @param type The type of analytic being tracked. Possible values are:
     * - `Identify`: Represents an identify event.
     * - `Screen`: Represents a screen tracking event.
     * - `Event`: Represents a custom event.
     * @param value The primary value of the analytic being tracked:
     * - For `Identify`: The user ID.
     * - For `Screen`: The screen title.
     * - For `Event`: The event name.
     * @param properties Additional context or metadata associated with the analytic, provided as a `Payload` object.
     */
    public fun didTrack(type: UserpilotAnalytic, value: String, properties: Payload)
}

public enum class UserpilotAnalytic {
    Identify,
    Screen,
    Event
}

Experience Listener

Receives callbacks when Userpilot experiences start, complete, or are dismissed, as well as changes in their step-by-step progression. Implement this if you want to pipe this data to a destination, or react to user action.
public interface UserpilotExperienceListener {

    /**
     * Notifies the listener of changes in the state of a Userpilot experience.
     *
     * @param experienceType The current experience type.
     * @param experienceId A unique identifier for the experience.
     * @param experienceState The current state of the experience.
     */
    public fun onExperienceStateChanged(
        experienceType: UserpilotExperienceType,
        experienceId: Int?,
        experienceState: UserpilotExperienceState
    )

    /**
     * Notifies the listener when the state of a specific step in a Userpilot experience changes.
     *
     * @param experienceType The current experience type.
     * @param experienceId A unique identifier for the experience.
     * @param stepId A unique identifier for the experience step.
     * @param stepState The current step state of the experience.
     * @param step The index of current step in the experience.
     * @param totalSteps The total number of steps in the experience.
     */
    @Suppress("LongParameterList")
    public fun onExperienceStepStateChanged(
        experienceType: UserpilotExperienceType,
        experienceId: Int,
        stepId: Int,
        stepState: UserpilotExperienceState,
        step: Int?,
        totalSteps: Int?
    )
}

/**
 * Enum representing the possible states of a Userpilot experience.
 */
public enum class UserpilotExperienceType {
    Flow,
    Survey,
    NPS
}

/**
 * Enum representing the possible states of a Userpilot experience.
 */
public enum class UserpilotExperienceState {
    /**
     * Indicates that the experience has started.
     */
    Started,

    /**
     * Indicates that the experience has successfully completed.
     */
    Completed,

    /**
     * Indicates that the experience was dismissed before completion.
     */
    Dismissed,

    /**
     * Indicates that the  experience/step was skipped.
     */
    Skipped,

    /**
     * Indicates that the experience/step was Submitted.
     */
    Submitted
}

Push Notifications

Userpilot SDK supports handling push notifications to help you deliver targeted messages and enhance user engagement.

Prerequisites

It is recommended to configure your Android push settings in the Userpilot Settings Studio before setting up push notifications in your app. To obtain the required keys and configuration details, please refer to the Android Push Notification Guide.

Setting up

This guide assumes this is the first time code is being added to your project related to setting up push notifications using Google services (Firebase Cloud Messaging). In the case your project is already configured for push, proceed to Step 2.
Step 1. Add Firebase
Follow the steps in the official Google documentation on How to add Firebase to your project.
Step 2. Request Notifications Permission
Starting from Android 13 (API level 33), apps **must explicitly request the **POST_NOTIFICATIONS permission in order to display push notifications.
  • Declare the Permission in the Manifest
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
  • Request the Permission at Runtime
private val requestPermissionLauncher = 
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
        if (isGranted) {
            // Permission granted: FCM and your app can post notifications.
        } else {
            // Permission denied: Inform the user they won’t receive notifications.
        }
    }

private fun requestNotificationPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        when {
            ContextCompat.checkSelfPermission(
                this, Manifest.permission.POST_NOTIFICATIONS
            ) == PackageManager.PERMISSION_GRANTED -> {
                // Permission already granted.
            }

            shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> {
                // Show an educational UI explaining why the permission is needed.
                // Once the user acknowledges, request the permission via `requestPermissionLauncher`.
            }

            else -> {
                // Directly request the permission.
                requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
            }
        }
    }
}
Step 3. Add the Userpilot Firebase Messaging Service Firebase connects to your app through a <service> . Go to your Manifest file and add:
 <service
    android:name="com.userpilot.pushNotifications.UserpilotFirebaseMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
If your project has already added a child of FirebaseMessagingService, you can leave it as is, and instead plug in UserpilotFirebaseMessagingService to your service class.
import com.userpilot.pushNotifications.UserpilotFirebaseMessagingService
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

class CustomFirebaseMessageService : FirebaseMessagingService() {
    override fun onMessageReceived(message: RemoteMessage) {
        if (UserpilotFirebaseMessagingService.handleMessage(baseContext, message)) {
           // handled as Userpilot message
           return
        }

        // not Userpilot message
        super.onMessageReceived(message)
        }

    override fun onNewToken(token: String) {
       // sets new token from the callback
       UserpilotFirebaseMessagingService.setToken(token)

       super.onNewToken(token)
    }
}

Step 4. Deep Linking: Launching Your App When a Notification is Clicked
To ensure your app opens automatically when a Userpilot push notification is clicked, you need to configure an intent-filter in your app’s **entry **Activity in Manifest.xml . This configuration enables deep linking based on a custom scheme defined in your app. Define the Deep Link Scheme In your userpilot.xml configuration file, you should have defined a value for userpilot_push_notification , check Step 5. Update Manifest file to include the intent-filter as below. Make sure to set host="sdk" .
<activity
    android:name=".features.main.MainActivity"
    android:exported="true"
    android:launchMode="singleTop">

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

    <!-- Intent filter for push notification deep linking -->
    <intent-filter>
        <data android:scheme="userpilot_push_notification" android:host="sdk" />
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
    </intent-filter>
</activity>
Inside your entry point activity, pass intent to userpilot SDK to handle it.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
  
    handleIntentUserpilotIntent(intent)
}


override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)

    if (handleIntentUserpilotIntent(intent)) return
}


 /** If not handled by Userpilot, then let app handle it. */
private fun handleIntentUserpilotIntent(intent: Intent): Boolean {
    return userpilot.onNewIntent(intent)
}

Step 5. Customizing
The Userpilot SDK allows for some customizations that will change how your messages will be delivered to your end users. These properties are set as <resources> properties. In order change those properties create a file under res/values and set it to desired values.
<resources>
    <!-- Small icon displayed in the notification bar -->
    <drawable name="userpilot_notification_small_icon">@drawable/ic_notification</drawable>

    <!-- Accent color applied to notification UI elements -->
    <color name="userpilot_notification_color">#6765E8</color>

    <!-- Determines if notifications are visible on the lock screen (true = visible) -->
    <bool name="userpilot_notification_channel_lock_screen_visibility">true</bool>

    <!-- Enables notification lights for the channel -->
    <bool name="userpilot_notification_channel_enable_light">true</bool>

    <!-- Enables vibration for the notifications -->
    <bool name="userpilot_notification_channel_enable_vibration">true</bool>

    <!-- Unique ID for the notification channel -->
    <string name="userpilot_notification_channel_id">com.userpilot.general.channel</string>

    <!-- Human-readable name for the notification channel -->
    <string name="userpilot_notification_channel_name">General</string>

    <!-- Description shown to the user about this notification channel -->
    <string name="userpilot_notification_channel_description">Default channel used for app messages</string>

    <!-- Importance level of the notification channel (0 = NONE, 5 = MAX) -->
    <integer name="userpilot_notification_channel_importance">3</integer>

    <!-- Key used to identify Userpilot push notifications -->
    <string name="userpilot_push_notification">userpilot_push_notification</string>
</resources>