Android (Kotlin)
Install and wire the Rovenue SDK in an Android/Kotlin app.
Add the dependency
Add the SDK to your module-level build.gradle.kts:
dependencies {
implementation("io.rovenue:sdk:0.16.0")
}Or in Groovy DSL (build.gradle):
dependencies {
implementation 'io.rovenue:sdk:0.16.0'
}The SDK drives Play Billing natively. Receipt processing and server-side validation happen automatically via the Rovenue API — you do not need to call Play Billing directly.
Configure the SDK
Call Rovenue.configure once in your Application.onCreate(), before any component starts:
import android.app.Application
import dev.rovenue.sdk.Rovenue
import dev.rovenue.sdk.generated.LogLevel
class App : Application() {
override fun onCreate() {
super.onCreate()
val versionName = packageManager
.getPackageInfo(packageName, 0)
.versionName
Rovenue.configure(
apiKey = "rov_pub_YOUR_API_KEY",
logLevel = LogLevel.INFO,
appVersion = versionName,
context = this,
)
}
}Pass context (the Application itself works, or any Context — the SDK
keeps context.applicationContext). Without it, getOfferings() still
works, but purchase() and restorePurchases() throw
RovenueException(kind = ErrorKind.STORE_PROBLEM) — Play Billing needs a
Context to bind to, and there is no fallback.
Self-hosting? baseUrl is optional and defaults to the hosted endpoint https://api.rovenue.io. Override it to point at your own deployment, e.g. Rovenue.configure(apiKey = "rov_pub_YOUR_API_KEY", baseUrl = "https://api.acme.com", appVersion = versionName). The host must use https:// — plain http:// is accepted only for localhost during local development.
Register App in your AndroidManifest.xml:
<application android:name=".App" ...>Types on this page
// Kotlin mirror of Rovenue.configure's parameters — there is no separate
// RovenueConfig type on this platform, only named parameters.
fun configure(
apiKey: String,
baseUrl: String? = null, // optional — defaults to https://api.rovenue.io
logLevel: LogLevel = LogLevel.WARN, // OFF | ERROR | WARN | INFO | DEBUG | TRACE
appVersion: String? = null,
context: Context? = null, // required for purchase()/restorePurchases()
environment: String? = null,
)| Field | Type | Description |
|---|---|---|
apiKey | String | Public SDK key from your Rovenue dashboard. Prefix rov_pub_. |
baseUrl | String? | Optional. Defaults to https://api.rovenue.io. Override only when self-hosting (https://; http:// allowed for localhost only). |
logLevel | LogLevel | Verbosity of SDK logs. Defaults to WARN — turn up to INFO/DEBUG locally, never ship DEBUG/TRACE in production. |
appVersion | String? | Overrides the version read from the package manager. |
context | Context? | An Android Context (the SDK stores context.applicationContext). Pass it — purchase() and restorePurchases() throw STORE_PROBLEM without it. |
environment | String? | Optional environment label forwarded with requests. |
Call configure in Application.onCreate — not inside an Activity. Calling
any other SDK method before configure has run throws a plain
IllegalStateException — not a RovenueException — so catch (e: RovenueException) will not observe it. This is a programming error to fix by
ordering (always configure first), not a condition to catch. See Calling
the SDK before configure().
Wire foreground signals
Use ProcessLifecycleOwner from androidx.lifecycle to observe the process-level foreground state. Add the observer in Application.onCreate, after the configure call:
import android.app.Application
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import dev.rovenue.sdk.Rovenue
class App : Application() {
override fun onCreate() {
super.onCreate()
val versionName = packageManager
.getPackageInfo(packageName, 0)
.versionName
Rovenue.configure(
apiKey = "rov_pub_YOUR_API_KEY",
appVersion = versionName,
context = this,
)
ProcessLifecycleOwner.get().lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Rovenue.shared.setForeground(true)
}
override fun onStop(owner: LifecycleOwner) {
Rovenue.shared.setForeground(false)
}
}
)
}
}ProcessLifecycleOwner fires onStart/onStop at the process level — not per-Activity. This is the correct granularity for the SDK's polling cadence.
Purchases
Use getOfferings() to fetch products configured in your Rovenue dashboard, then purchase(pkg) to drive the full Play Billing flow. The SDK validates the receipt server-side and resolves with updated entitlements.
import dev.rovenue.sdk.Rovenue
import dev.rovenue.sdk.RovenueException
import dev.rovenue.sdk.generated.ErrorKind
val offerings = Rovenue.shared.getOfferings()
val pkg = offerings.current?.packages?.firstOrNull()
if (pkg != null) {
try {
// `activity` is your current Activity — Play Billing requires it.
val result = Rovenue.shared.purchase(activity, pkg)
if (result.isDeferred) {
// Ask-to-Buy / parental-control hold — no entitlements yet. The SDK
// reconciles it in the background; collect Rovenue.shared.changes for
// ChangeEvent.ENTITLEMENTS_CHANGED to know when it resolves.
} else {
// result.entitlements and result.virtualCurrencies are already up to date
}
} catch (e: RovenueException) {
when (e.kind) {
ErrorKind.PURCHASE_CANCELED -> Unit // User dismissed the store sheet — no action needed
else -> throw e
}
}
}
// Re-validate all prior transactions (e.g. after reinstall):
Rovenue.shared.restorePurchases(activity)// Kotlin mirror of PurchaseResult
data class PurchaseResult(
val entitlements: List<Entitlement>,
val virtualCurrencies: Map<String, Long>,
val productId: String,
val storeTransactionId: String,
val isDeferred: Boolean, // true for an Ask-to-Buy / parental-control hold
)
data class Entitlement(
val id: String,
val isActive: Boolean,
val productIdentifier: String,
val store: String,
val expiresIso: String?, // ISO 8601 — present for time-limited entitlements
)| Field | Type | Description |
|---|---|---|
entitlements | List<Entitlement> | Updated entitlement list after the purchase. |
virtualCurrencies | Map<String, Long> | Updated virtual-currency balances after the purchase (currency code → amount). |
productId | String | The product identifier that was purchased. |
storeTransactionId | String | The Play Store transaction ID. |
isDeferred | Boolean | true when the purchase is pending external approval — no entitlements are granted yet. |
Who acknowledges a Play purchase
The SDK is the only party that acknowledges (subscriptions, non-consumables) or consumes (consumables) a Google Play purchase; the Rovenue server never does. It does so on the device, only after the server has verified the receipt, and it re-checks the token's state with Play first, so a purchase that is already acknowledged is left alone. Once the server has verified the purchase, purchase() returns its PurchaseResult and never throws: if Play cannot take the acknowledgement right then (the device went offline, Play Billing is unavailable), the SDK retries a few times and then leaves it to the next reconcile, which runs on every app launch and foreground — well inside Play's three-day window before it auto-refunds an unacknowledged purchase. A verification failure is never acknowledged, so Play refunds that purchase as intended.
There is no PurchaseCancelledException/PurchasePendingException — every SDK method throws the single RovenueException, discriminated by .kind. And there is no PURCHASE_PENDING kind at all: as of 0.16.0, a deferred purchase does not throw; it returns normally with isDeferred = true. See Reference: Errors for the full 24-value ErrorKind taxonomy and Processing Purchases for the complete purchase API and the StoreProduct shape.
Funnels
getFunnel(placementId, locale) resolves a placement into the funnel the
current subscriber should see (see Placements &
Funnels). The session is already open
when it returns — hand it to a FunnelDriver and host RovenueFunnelView
(see Show a Funnel):
val funnel = Rovenue.shared.getFunnel(placementId = "onboarding", locale = "en")
if (funnel != null) {
// null when the placement has no assignment — render nothing.
// Hand `funnel` to a FunnelDriver and host RovenueFunnelView (Show a Funnel).
println(funnel)
}Purchase attribution is automatic: the funnel's presentedContext is
attached to the next purchase for you.
See Placements for how placements resolve to a funnel or an A/B experiment variant.
Observing changes
Rovenue.shared.changes is a SharedFlow of change events. Collect it from a coroutine scope tied to your component's lifecycle:
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.Lifecycle
import dev.rovenue.sdk.Rovenue
// Inside an Activity or Fragment:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
Rovenue.shared.changes.collect { event ->
// event: ChangeEvent (ENTITLEMENTS_CHANGED | IDENTITY_CHANGED | VIRTUAL_CURRENCIES_CHANGED)
println("Rovenue change: $event")
}
}
}Collect Rovenue.shared.changes from an Activity or ViewModel scope — not from inside the SDK's own internal scope. Calling Rovenue.shared.shutdown() from inside an active collector's coroutine may interrupt the collector mid-flight.
Suspend functions
SDK methods such as identify, logOut, refreshEntitlements, and refreshVirtualCurrencies are suspend functions. Call them from a coroutine:
lifecycleScope.launch {
// After your auth flow completes:
Rovenue.shared.identify("user_123")
// When your user signs out:
Rovenue.shared.logOut()
val entitlements = Rovenue.shared.refreshEntitlements()
}See Identifying Users for the full identity model, including what happens when identify and logOut are called.
Funnels
A placement can serve a native funnel: getFunnel, then a FunnelDriver
bound to RovenueFunnelView (packages/sdk-kotlin/src/main/kotlin/dev/rovenue/sdk/Rovenue.kt:631) — full path: Show a funnel.
import dev.rovenue.sdk.funnelui.FunnelDriver
import dev.rovenue.sdk.funnelui.UniffiFunnelSession
val funnel = Rovenue.shared.getFunnel(placementId = "onboarding")
if (funnel != null) {
val driver = FunnelDriver(UniffiFunnelSession(funnel.session))
println(driver.stateJson())
}What's next
- Identifying Users — link your backend user to the Rovenue subscriber.
- Entitlements — read and gate on active subscriptions.
- Virtual Currencies — read virtual-currency balances and grant/spend via server-side APIs.
- Processing Purchases — full purchase API and error handling.
- App Lifecycle — foreground/background wiring in depth.
- Reference: Methods — full method reference.
- Reference: Types — all SDK types in one place.
- SDK API Reference — the generated Dokka reference for every public Kotlin type and method, at
/api/sdk-kotlin/.