Add Sumly to an Android app

One Kotlin file with no dependencies. It sends screens and events under the person RevenueCat knows, and with Compose Navigation every change of screen counts by itself.

Drawn with an example line.

Not yet run in a real app. Sumly.kt builds and follows the same rules as the React Native file, which runs in our own apps, but it has not run in a shipped app yet. Try it in a test build first, and tell us if anything is off.

Step by step

  1. 1

    In Sumly, open Visitors and copy your project's public key (sl_…). Download https://sumly.dev/sdk/Sumly.kt into your app module (change its package line if you like). The app needs the INTERNET permission, which almost every app has.

  2. 2

    In Application.onCreate, call Sumly.init(this, siteKey = "sl_…"). After Purchases.configure, and again after each logIn or logOut, call Sumly.identify(Purchases.sharedInstance.appUserID).

  3. 3

    Count screens: with Navigation Compose, a listener on the NavController names each screen as it opens; anywhere else, Sumly.screen("Paywall").

In your Application class.

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Sumly.init(this, siteKey = "sl_your_key")
        Purchases.configure(PurchasesConfiguration.Builder(this, "goog_…").build())
        Sumly.identify(Purchases.sharedInstance.appUserID)
    }
}

// after signing in or out
Purchases.sharedInstance.logInWith(user.id) { _, _ ->
    Sumly.identify(Purchases.sharedInstance.appUserID)
}

Or let your coding assistant do it

Paste this into Claude Code, Cursor or Codex in your project. It fits the code to how your project is built, asks you for the dashboard steps and leaves the keys to you.

See what it says
Set up Kotlin and Jetpack Compose for Sumly in this project.

Sumly (https://sumly.dev) shows revenue next to the visits and app users it came from. The full guide is https://sumly.dev/install/kotlin; read it if anything below is unclear.

Steps:
1. In Sumly, open Visitors and copy your project's public key (`sl_…`). Download https://sumly.dev/sdk/Sumly.kt into your app module (change its package line if you like). The app needs the INTERNET permission, which almost every app has.
2. In `Application.onCreate`, call `Sumly.init(this, siteKey = "sl_…")`. After `Purchases.configure`, and again after each `logIn` or `logOut`, call `Sumly.identify(Purchases.sharedInstance.appUserID)`.
3. Count screens: with Navigation Compose, a listener on the NavController names each screen as it opens; anywhere else, `Sumly.screen("Paywall")`.

Setup: In your Application class.
```
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Sumly.init(this, siteKey = "sl_your_key")
        Purchases.configure(PurchasesConfiguration.Builder(this, "goog_…").build())
        Sumly.identify(Purchases.sharedInstance.appUserID)
    }
}

// after signing in or out
Purchases.sharedInstance.logInWith(user.id) { _, _ ->
    Sumly.identify(Purchases.sharedInstance.appUserID)
}
```

Compose screens: The listener gets the screen it is on as soon as it is added, then each new one.
```
val navController = rememberNavController()

DisposableEffect(navController) {
    val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
        Sumly.screen(destination.route ?: "App")
    }
    navController.addOnDestinationChangedListener(listener)
    onDispose { navController.removeOnDestinationChangedListener(listener) }
}

// events
Sumly.track("trial_started", mapOf("plan" to "yearly"))
```

Consent: Where your app asks first.
```
Sumly.init(this, siteKey = "sl_your_key", waitForConsent = true)

Sumly.consent(true)    // when the person agrees
Sumly.consent(false)   // when they refuse, or take it back
```

Own domain: Optional: your own address takes app events too.
```
Sumly.init(this, siteKey = "sl_your_key", endpoint = "https://signal.example.com")
```

Keep in mind: With type-safe routes (Navigation 2.8 and later) a destination's route is the route class's name, which may be long; give `Sumly.screen` a shorter name of your own if you like. No advertising id. In Google Play's Data safety, declare app interactions, user ids and approximate location, collected for analytics. The queue is kept in SharedPreferences. With ACCESS_NETWORK_STATE it sends as soon as the network is back; without it, on its timer.

How to work:
- Look at how this project is built first, and fit the code to its framework and its way of doing things; the examples above are a starting point, not something to paste as is.
- Steps that happen in a dashboard (Sumly, or Kotlin and Jetpack Compose's own) are mine: tell me exactly what to click, then wait for me.
- Keys the code needs belong in environment variables, never in the code or the repository. Never ask me to paste a secret key into this chat; tell me which variable to set.
- Change only what this needs, show me the changes before you commit, and tell me how to check that it works.
- sl_your_key stands for the project's public key; it is on the Visitors page in Sumly. Ask me for it.

Consent

Where your app asks first, start with waitForConsent: nothing is kept or sent until consent(true), and consent(false) stops sending and empties the queue. The answer is remembered.

Purchases

Purchases do not come through the app: Sumly reads them from RevenueCat, Adapty or Superwall. Give identify the same person they know: RevenueCat's app user id (the anonymous one too), or the user id you pass to Adapty.identify or Superwall.identify. Each purchase then shows the screens and the source that came before it.

Your own domain

Optional. The own address of your site's script (one DNS record on Visitors, on Starter and Growth) takes app events too: give it as the endpoint and the app sends to /api/v1/events there.

Good to know

  • With type-safe routes (Navigation 2.8 and later) a destination's route is the route class's name, which may be long; give Sumly.screen a shorter name of your own if you like.
  • No advertising id. In Google Play's Data safety, declare app interactions, user ids and approximate location, collected for analytics.
  • The queue is kept in SharedPreferences. With ACCESS_NETWORK_STATE it sends as soon as the network is back; without it, on its timer.

From Kotlin and Jetpack Compose's own documentation, checked 4 November 2026: developer.android.com/guide/navigation/integrations/ui, developer.android.com/reference/kotlin/androidx/navigation/NavController, revenuecat.com/docs/customers/identifying-customers .

Other apps

Try it with your own projects

Sign in with Google or a code by e-mail and connect your first project. Free for one project, and 14 days of a paid plan with no card.

Start free

Or look around first: open the live demo.