Instrument and configure the Android-Kotlin SDK

This guide covers the everyday work with the Android-Kotlin SDK after Get started: tracking your own events, managing identity and sessions, choosing Autocapture features, controlling how events reach Amplitude, and extending the SDK with plugins. For exact signatures and every option, go to the Reference. To find the release that added or changed a feature, go to Android-Kotlin SDK release notes.

The samples use an instance named amplitude, created as in Initialize the SDK.

Track events

Track a custom event

Pass an event name, and optionally a map of event properties:

amplitude.track("Song Played", mapOf("title" to "Happy Birthday", "duration_s" to 42))

Don't track push notification events from the device. The SDK uploads only while the app runs, so a notification event can reach Amplitude long after it happened. Send those events from your server instead.

Pass the event as an object

Build a BaseEvent to set fields that the name-and-properties form doesn't take, such as event-level groups or your own insertId for deduplication:

kotlin
import com.amplitude.core.events.BaseEvent

val event = BaseEvent().apply {
    eventType = "Song Played"
    eventProperties = mutableMapOf("title" to "Happy Birthday")
    groups = mutableMapOf("orgId" to "15")
    insertId = "song-played-1234"
}
amplitude.track(event)

Override fields for one event

Pass EventOptions to set fields such as userId, timestamp, or sessionId for one event only:

kotlin
import com.amplitude.core.events.EventOptions

amplitude.track(
    "Purchase Synced",
    mapOf("order_id" to "A-100"),
    EventOptions().apply { timestamp = orderTimeMillis },
)

Get the upload result for an event

Pass a callback with a BaseEvent. The SDK calls it when it finishes with the event: status is 200 when Amplitude accepted it, or the error status when the SDK dropped it.

amplitude.track(BaseEvent().apply { eventType = "Button Clicked" }) { event, status, message ->
    Log.d("MyApp", "${event.eventType}: $status $message")
}

To get results for every event, set the callback configuration option instead. For the statuses, go to Read the upload result.

Set user properties

Build an Identify with one or more operations and pass it to identify():

import com.amplitude.core.events.Identify

val identify = Identify()
    .set("color", "green")
    .setOnce("initial_source", "organic")
    .add("login_count", 1)
    .append("visited_pages", "home")
    .unset("temporary_property")
amplitude.identify(identify)

For every operation, go to Identify. Use each property once per Identify; the SDK ignores a second operation on the same property. clearAll() removes every user property permanently.

To reduce the number of events it sends, the SDK holds identify events that contain only set operations and merges them. It sends the merged event after identifyBatchIntervalMillis (30 seconds by default), or right before the next other event, whichever comes first.

User property changes appear on the user's profile right away. Charts apply them to events the user sends afterward, not to earlier events.

Assign a user to a group

Groups let you analyze by an account or organization, for example counting distinct organizations that performed an event. setGroup() assigns the current user to a group and sets groupType as a user property, replacing any earlier value for that type.

amplitude.setGroup("orgId", "15")
amplitude.setGroup("sport", arrayOf("tennis", "soccer")) // more than one group

To put one event in a group without changing the user, set groups on a BaseEvent. Go to Pass the event as an object.

Set group properties

kotlin
amplitude.groupIdentify("plan", "enterprise", Identify().set("seats", 50))

Group property changes apply to events sent afterward. Amplitude supports up to 5 group types per event and 10 groups in total.

Track revenue

Create a Revenue with at least price, and pass it to revenue(). The SDK sends a revenue_amount event.

import com.amplitude.core.events.Revenue

val revenue = Revenue().apply {
    productId = "com.example.premium"
    price = 3.99
    quantity = 3
}
amplitude.revenue(revenue)

For revenue verification, set revenueType and call revenue.setReceipt(receipt, receiptSignature), which sets receipt and receiptSig. For every field, go to Revenue.

Identity and sessions

How the SDK chooses identifiers on startup

Device ID

On startup, the SDK uses the first of these that applies:

  1. The deviceId configuration option, if you set it.
  2. The device ID it stored on an earlier run, unless that ID came from the App Set ID.
  3. The advertising ID, if useAdvertisingIdForDeviceId is true, newDeviceIdPerInstall is false, and the user hasn't limited ad tracking.
  4. The App Set ID with S appended, if useAppSetIdForDeviceId is true.
  5. A random UUID with R appended.

Because the SDK stores the device ID with the app's data, a backup restored to a new device can bring the old device ID with it. Set the same user ID on every device a user signs in to, so Amplitude merges their devices into one user.

User ID

The SDK restores the user ID it stored on an earlier run. A setUserId() call made before setup finishes wins over the stored value.

Set the user ID

Set the user ID when a user signs in. null clears it.

kotlin
amplitude.setUserId("user@example.com")
val userId = amplitude.getUserId()

Reset identity when a user signs out

reset() clears the user ID and picks a device ID again, so later events count as a new anonymous user:

kotlin
amplitude.reset()

reset() doesn't start a new session. It skips only the stored device ID, so it keeps the same device ID when you set the deviceId configuration option, useAdvertisingIdForDeviceId, or useAppSetIdForDeviceId. Amplitude then links events before and after the reset through that device ID. To start a separate anonymous user, set a new device ID after the reset:

kotlin
amplitude.reset()
amplitude.setDeviceId(UUID.randomUUID().toString())

Set the device ID

Use a value that's unique per device, such as a UUID:

kotlin
amplitude.setDeviceId(UUID.randomUUID().toString())

Read and update identity together

amplitude.identity returns the user ID, device ID, and local user properties. setIdentity() replaces all three in one step, so plugins such as Experiment see one change instead of several. Read the device ID after setup finishes:

kotlin
// Inside a coroutine
amplitude.isBuilt.await()
amplitude.setIdentity("user-123", amplitude.getDeviceId(), mapOf("plan" to "paid"))

If the properties change, the SDK sends one identify event that sets them. The local properties live only in memory for the current process. Removing a property from the map doesn't remove it in Amplitude. To remove it, send Identify().unset(). Passing null as the device ID clears it. Requires 1.36.0 or later.

How sessions work

A session starts when the app comes to the foreground, or when you track an event while no session is active. The session continues while the app is in the foreground. When the app goes to the background, the session ends after minTimeBetweenSessionsMillis (5 minutes by default) passes without an event. The next return to the foreground, or the next event, then starts a new session. An event tracked in the background within that time extends the session.

Events in a session share a session_id, which is the session's start time in milliseconds. With SESSIONS autocapture on (the default), the SDK also tracks session_start and session_end events. With it off, the SDK still assigns session IDs.

From 1.34.0, the SDK detects foreground and background from the process lifecycle, so multi-activity apps and apps that start a service before any activity get correct sessions.

Some session data can look incomplete:

  • If the user never returns to the app, the SDK sends no session_end for that session.
  • Events tracked in the background make a session look longer than the time the user spent in the app.
  • A session_end sent after a user property change carries the new property values. To stop Amplitude from applying the event's user properties, set $skip_user_properties_sync on it with an enrichment plugin.

Change the session timeout

kotlin
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    minTimeBetweenSessionsMillis = 10_000
}

Stop sending session events

Set autocapture without SESSIONS. Events still get session IDs.

kotlin
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    autocapture = emptySet()
}

Track an event outside a session

Set sessionId = -1 in EventOptions. The event doesn't belong to, extend, or start a session, and doesn't change the session ID of later events. Use it for events the user didn't trigger in the app, such as a background sync.

kotlin
val outOfSession = EventOptions().apply { sessionId = -1 }
amplitude.track(BaseEvent().apply { eventType = "Sync Completed" }, outOfSession)
amplitude.identify(Identify().set("synced", true), outOfSession)

Opt users out of tracking

While optOut is true, the SDK drops every event and doesn't store it.

amplitude.optOut = true  // Stop tracking
amplitude.optOut = false // Resume tracking

To start opted out, set optOut = true in the configuration.

Control which properties the SDK collects

Create a TrackingOptions, turn off the fields you don't want, and set it in the configuration:

import com.amplitude.android.TrackingOptions

val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    trackingOptions = TrackingOptions().disableIpAddress().disableLatLng().disableCarrier()
}

Amplitude derives city, region, and DMA from the IP address, so disableIpAddress() is what stops them. disableCity(), disableRegion(), disableDma(), and disableApiLevel() change nothing on the device. Turning a field off affects new events only; it doesn't remove data Amplitude already has. For every method, go to TrackingOptions.

Turn on COPPA control

For apps that must comply with the Children's Online Privacy Protection Act (COPPA), set enableCoppaControl = true. COPPA control stops the SDK from collecting the advertising ID, IP address, and latitude and longitude. Without the IP address, Amplitude also stops deriving city. COPPA control doesn't stop the App Set ID. To stop the App Set ID too, also call disableAppSetId().

Use the advertising ID or app set ID

The SDK can use a Google Play identifier as the device ID. Users can reset or turn off the advertising ID, and the advertising ID can be null. For most apps, the App Set ID is the more stable choice.

Advertising ID

  1. Add the Play services dependency:

    kotlin
    implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
    
  2. If your app targets Android 13 or later, declare the permission in AndroidManifest.xml. The SDK doesn't declare it for you.

    xml
    <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
    
  3. If you minify with R8 or ProGuard, keep the classes the SDK loads by reflection:

    text
    -keep class com.google.android.gms.ads.** { *; }
    -keep class com.google.android.gms.common.** { *; }
    
  4. Turn on the advertising ID:

    kotlin
    val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
        useAdvertisingIdForDeviceId = true
    }
    

App Set ID

The App Set ID identifies an app install on a device without being tied to advertising. It resets after 13 months without use.

  1. Add the dependency. The SDK ships keep rules for it.

    kotlin
    implementation("com.google.android.gms:play-services-appset:16.0.2")
    
  2. Turn on the App Set ID:

    kotlin
    val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
        useAppSetIdForDeviceId = true
    }
    

Track location

By default, Amplitude derives location from the IP address. To add the device's last known latitude and longitude, set locationListening = true, and request ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION from the user. The SDK adds coordinates only while the app has the permission.

kotlin
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    locationListening = true
}

The default is false. Default changed in 1.20.7.

Autocapture

Choose autocapture features

By default, the SDK tracks session events only. To turn on more features, set autocapture. Requires 1.17.0 or later.

import com.amplitude.android.autocaptureOptions

val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    autocapture = autocaptureOptions {
        +sessions
        +appLifecycles
        +screenViews
        +deepLinks
        +elementInteractions
        +frustrationInteractions
    }
}

Setting autocapture replaces the default set, so include +sessions to keep session events. In Java, getAutocapture() already contains SESSIONS; call remove(AutocaptureOption.SESSIONS) to drop it. AutocaptureOption.ALL turns on everything.

With enableAutocaptureRemoteConfig on (the default), autocapture settings saved in Amplitude override the local set. Settings that Amplitude doesn't send keep their local values.

Features and events

App lifecycles

Screen views

The SDK tracks [Amplitude] Screen Viewed for each activity and [Amplitude] Fragment Viewed for each fragment. Fragment tracking needs androidx.fragment in your app and requires 1.18.0 or later.

The SDK tracks [Amplitude] Deep Link Opened with [Amplitude] Link URL and [Amplitude] Link Referrer when an activity starts or resumes with an intent that has data.

If the activity uses the singleTop, singleTask, or singleInstance launch mode, Android delivers a new link to onNewIntent() without restarting the activity. Call setIntent() there, or the SDK keeps reading the original intent:

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

Element interactions

The SDK tracks [Amplitude] Element Interacted when a user interacts with an element, in both Android Views and Jetpack Compose. From 1.35.0, it also tracks non-tap gestures such as swipes and long presses, and selectable Compose controls such as Checkbox and Switch.

Compose elements have no class, resource, text, or hierarchy, so add Modifier.testTag to the composables you want to tell apart. A tag also stays stable when you restyle the UI:

kotlin
Button(onClick = { /* ... */ }, modifier = Modifier.testTag("login_button")) {
    Text("Log In")
}

Frustration interactions

FRUSTRATION_INTERACTIONS tracks two events. Requires 1.22.0 or later.

  • [Amplitude] Rage Click: four or more clicks on the same element within one second, no more than 50 dp apart. The event reports every click in the burst.
  • [Amplitude] Dead Click: a click on an interactive element followed by no visible change within three seconds. Dead click detection needs a plugin that reports UI changes, which the Session Replay Android plugin provides. Without one, the SDK tracks no dead clicks.

Both events carry the Element interactions properties, plus:

To track only one of the two, set interactionsOptions:

kotlin
import com.amplitude.android.DeadClickOptions
import com.amplitude.android.InteractionsOptions
import com.amplitude.android.RageClickOptions

val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    autocapture = autocaptureOptions { +frustrationInteractions }
    interactionsOptions = InteractionsOptions(
        rageClick = RageClickOptions(enabled = true),
        deadClick = DeadClickOptions(enabled = false),
    )
}

Exclude elements from frustration tracking

Users tap some elements quickly by design, such as stepper buttons. Other elements never change the screen, such as a close button. Exclude these elements from rage or dead click detection. The SDK still tracks [Amplitude] Element Interacted for them.

import com.amplitude.android.FrustrationAnalyticsUtils
import com.amplitude.android.ignoreFrustrationAnalytics

// Views: ignore both, or only rage clicks
FrustrationAnalyticsUtils.ignoreFrustrationAnalytics(backButton)
FrustrationAnalyticsUtils.ignoreFrustrationAnalytics(incrementButton, rageClick = true, deadClick = false)
FrustrationAnalyticsUtils.unignoreView(backButton)

// Compose: ignore only dead clicks
Button(
    onClick = { submitForm() },
    modifier = Modifier.ignoreFrustrationAnalytics(rageClick = false, deadClick = true),
) { Text("Submit") }

rageClick = true ignores rage clicks and deadClick = true ignores dead clicks. Both default to true.

Network tracking

NetworkTrackingPlugin tracks [Amplitude] Network Request events for HTTP requests made through OkHttp. It isn't part of autocapture: add it as an interceptor on each OkHttp client you want to track, and add it to the instance. Requires 1.21.0 or later, and OkHttp in your app.

kotlin
import com.amplitude.android.network.NetworkTrackingPlugin

val networkPlugin = NetworkTrackingPlugin()
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(networkPlugin)
    .build()
amplitude.add(networkPlugin)

By default, the plugin captures responses with status 500 to 599 from every host except Amplitude. To choose hosts and statuses, pass NetworkTrackingOptions:

kotlin
import com.amplitude.android.network.NetworkTrackingOptions
import com.amplitude.android.network.NetworkTrackingOptions.CaptureRule

val options = NetworkTrackingOptions(
    captureRules = listOf(
        CaptureRule(hosts = listOf("*"), statusCodeRange = (400..599).toList()),
        CaptureRule(hosts = listOf("*.example.com", "example.com"), statusCodeRange = (500..599).toList()),
    ),
    ignoreHosts = listOf("analytics.example.com"),
)
amplitude.add(NetworkTrackingPlugin(options))

The plugin first skips ignoreHosts and, by default, Amplitude's own requests. It then checks rules from the last to the first, and the first rule whose hosts match decides. With the rules above, the plugin captures a 503 from example.com. It doesn't capture a 401 from example.com, because the last rule matched the host but not the status. It captures a 401 from other.com through the first rule.

To match by URL, filter by method, or capture headers and JSON body fields, use the URL-based rule. Requires 1.28.0 or later.

kotlin
import com.amplitude.android.network.NetworkTrackingOptions.CaptureBody
import com.amplitude.android.network.NetworkTrackingOptions.CaptureHeader
import com.amplitude.android.network.NetworkTrackingOptions.URLPattern

CaptureRule(
    urls = listOf(URLPattern.Regex("^https://api\\.example\\.com/v1/.*")),
    methods = listOf("POST"),
    statusCodeRange = (200..599).toList(),
    requestHeaders = CaptureHeader(allowlist = listOf("X-Request-Id")),
    responseHeaders = CaptureHeader(allowlist = listOf("X-Response-Id")),
    requestBody = CaptureBody(allowlist = listOf("user/*"), excludelist = listOf("**/password")),
    responseBody = CaptureBody(allowlist = listOf("profile/**")),
)

The plugin never captures the authorization, cookie, or proxy-authorization headers, and it masks credentials and common sensitive query parameters in URLs. For every option, go to NetworkTrackingPlugin.

Track media playback

To track Media3 and ExoPlayer playback, add com.amplitude:streaming-analytics-android at the same version as analytics-android. The SDK installs the package automatically when the package is on the classpath. Requires 1.33.0 or later. For changes to media playback events, go to the release notes.

Data delivery

How the SDK uploads events

The SDK writes each event to a file in the app's storage, then uploads queued events when:

  • 30 events (flushQueueSize) are waiting.
  • 30 seconds (flushIntervalMillis) pass after an event is queued.
  • The app goes to the background (flushEventsOnClose, on by default).
  • The device reconnects after being offline.
  • You call amplitude.flush().

Stored events survive app restarts. When an upload fails, the SDK keeps the events and retries with growing delays: about 1, 8, 32, and 128 seconds, then every 5 minutes, with no limit. After a 429, it waits 30 seconds. It drops events only when Amplitude rejects them, for example for an invalid API key. The deprecated flushMaxRetries option has no effect. Retries became unlimited in 1.31.0.

Tune upload performance

Raise flushIntervalMillis and flushQueueSize to upload less often, or lower them to see events sooner:

val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    flushIntervalMillis = 60_000
    flushQueueSize = 50
}

flushQueueSize sets when an upload starts, not how many events a request carries. To upload large volumes through the Batch API, set useBatch = true.

Choose a data region

For a project in Amplitude's EU data center, set serverZone. Use the API key from the EU project.

import com.amplitude.core.ServerZone

val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    serverZone = ServerZone.EU
}

Send events through a proxy

Set serverUrl to your proxy's URL. The proxy forwards the request to Amplitude's endpoint for your data center and returns Amplitude's response unchanged, so the SDK can read upload results.

kotlin
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    serverUrl = "https://analytics.example.com/2/httpapi"
}

The SDK sends plain JSON to a custom serverUrl. To compress the request body, set enableRequestBodyCompression = true; your proxy must accept Content-Encoding: gzip.

serverUrl covers event uploads only. The SDK still fetches remote configuration from sr-client-cfg.amplitude.com (EU: sr-client-cfg.eu.amplitude.com) and sends its diagnostics to Amplitude directly.

Work offline

The SDK holds events while the device has no network and uploads them when the device reconnects. Offline mode needs the ACCESS_NETWORK_STATE permission. Without it, the SDK logs No ACCESS_NETWORK_STATE permission, offline mode is not supported. ... and treats the device as always online, so uploads fail and retry instead. Requires 1.13.0 or later.

To control offline mode yourself, for example to hold uploads until the user gives consent, set offline = AndroidNetworkConnectivityCheckerPlugin.Disabled in the configuration. This setting removes the connectivity check. Then set amplitude.configuration.offline to true or false from your code.

Use a custom HTTP client

The SDK uploads with HttpURLConnection. To use another client, such as OkHttp for certificate pinning or custom timeouts, implement HttpClientInterface and set httpClient. Requires 1.20.0 or later. The SDK repository has a sample OkHttp client with a gzip interceptor.

val httpClient = CustomOkHttpClient()
val amplitude = Amplitude("YOUR_API_KEY", applicationContext) {
    this.httpClient = httpClient
}
httpClient.initialize(amplitude.configuration)

A custom client handles compression itself.

Plugins

Plugins run code on every event or react to identity changes. The SDK runs Before, then Enrichment, then Destination plugins for each event. A Before or Enrichment plugin returns the event, changed or not, or null to drop it. For every hook, go to Plugin.

Add properties to every event

import com.amplitude.core.Amplitude
import com.amplitude.core.events.BaseEvent
import com.amplitude.core.platform.Plugin

class AppFlavorPlugin : Plugin {
    override val type = Plugin.Type.Enrichment
    override lateinit var amplitude: Amplitude

    override fun execute(event: BaseEvent): BaseEvent {
        event.eventProperties = (event.eventProperties ?: mutableMapOf()).apply {
            put("app_flavor", BuildConfig.FLAVOR)
        }
        return event
    }
}

amplitude.add(AppFlavorPlugin())

To run before every other enrichment, for example to guard a field other plugins read, use Plugin.Type.Before.

Send events to another destination

Extend DestinationPlugin and override track(). Override identify(), groupIdentify(), revenue(), and flush() as needed. Don't override execute(); it's final on destination plugins.

kotlin
import com.amplitude.core.events.BaseEvent
import com.amplitude.core.platform.DestinationPlugin

class LogcatDestination : DestinationPlugin() {
    override fun track(payload: BaseEvent): BaseEvent {
        Log.d("Events", "${payload.eventType} ${payload.eventProperties}")
        return payload
    }
}

amplitude.add(LogcatDestination())

React to identity changes

Extend ObservePlugin to react to sign-in, sign-out, and other identity changes without touching events. Override onUserIdChanged and onDeviceIdChanged, plus any of onSessionIdChanged, onOptOutChanged, onIdentityChanged, and onReset.

kotlin
import com.amplitude.core.Amplitude
import com.amplitude.core.platform.ObservePlugin

class LoginObserver : ObservePlugin() {
    override lateinit var amplitude: Amplitude

    override fun onUserIdChanged(userId: String?) {
        // React to sign-in or sign-out.
    }

    override fun onDeviceIdChanged(deviceId: String?) {}
}

amplitude.add(LoginObserver())

Remove a plugin

Pass the same instance you added:

kotlin
val plugin = AppFlavorPlugin()
amplitude.add(plugin)
amplitude.remove(plugin)

Run more than one instance

To send events to more than one project, create one instance per project, each with its own instanceName. Each instance keeps its own storage, identity, and queue.

kotlin
val product = Amplitude("PRODUCT_API_KEY", applicationContext) {
    instanceName = "product"
}
val marketing = Amplitude("MARKETING_API_KEY", applicationContext) {
    instanceName = "marketing"
}

Create each instance once, for example in Application.onCreate, and inject it where you need it. With Hilt, Koin, or Dagger, provide it as an application-scoped singleton. Creating a second instance with the same instanceName retires the first one, which then drops new events with Dropping event '<type>': this Amplitude instance was replaced. Requires 1.30.1 or later.

Was this helpful?

For AI agents: a documentation index is available at /docs/llms.txt. Append .md to any page URL for markdown, or send Accept: text/markdown.

On this page