Kotlin Multiplatform SDK
Complete integration guide for the Encatch Kotlin Multiplatform SDK — in-app feedback and survey collection for KMP apps targeting Android and iOS
The Encatch Kotlin Multiplatform SDK (com.encatch:kmp-sdk) lets you collect in-app feedback and surveys from shared commonMain code. One Encatch object gives you the full SDK — initialize, identify users, track screens and events, show modal forms, and submit responses — with the same call site on Android and iOS and zero platform-bridging code of your own.
Under the hood it is a thin platform-routing layer over the two native Encatch SDKs, not a reimplementation: on Android it forwards 1:1 to the native Android SDK (Android's native language is Kotlin), and on iOS it forwards through Kotlin/Native cinterop to the pure-Swift iOS SDK.
Building with Compose Multiplatform?
This module is pure business logic with no UI layer. If your app uses Compose Multiplatform and you also want a ready-made inline-form composable, use the Compose Multiplatform SDK (com.encatch:compose-sdk) instead — it depends on this module, re-exports the same Encatch API, and adds EncatchInlineForm plus fully automatic modal-host setup.
Overview
- Package:
com.encatch:kmp-sdk(Maven Central) - Version: 0.1.1
- Platforms: Android (minSdk 24), iOS (
iosArm64,iosSimulatorArm64) - Repository: github.com/get-encatch/encatch-android
- License: MIT
Installation
Add the dependency to your shared module's commonMain source set:
// build.gradle.kts (shared module)
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.encatch:kmp-sdk:0.1.1")
}
}
}Platform setup
Install the modal form host once, typically in your Application.onCreate. This module cannot do it for you automatically — it has no Context/Application reference available from commonMain (unlike com.encatch:compose-sdk, which can do this lazily via Compose's LocalContext):
import android.app.Application
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
com.encatch.android.EncatchFormHost.install(this)
}
}Without this call, showForm cannot present the modal overlay on Android.
Nothing to do — Encatch.init(...) installs the modal form host automatically the first time it's called.
Quick Start
1. Initialization
Call Encatch.init once at app startup from any coroutine scope. It's a suspend function — all subsequent calls (identifyUser, showForm, tracking) silently no-op until initialization completes.
import com.encatch.sdk.Encatch
// commonMain — same call site on both platforms
scope.launch {
Encatch.init("your-api-key")
}Check Encatch.isInitialized to guard against double-initialization, e.g. on process restarts:
if (!Encatch.isInitialized) {
Encatch.init("your-api-key")
}Pass an optional EncatchConfig to customize SDK behavior:
import com.encatch.sdk.Encatch
import com.encatch.sdk.EncatchConfig
import com.encatch.sdk.Theme
scope.launch {
Encatch.init(
"your-api-key",
EncatchConfig(
theme = Theme.SYSTEM,
debugMode = true,
isFullScreen = false,
appVersion = "1.2.3",
onBeforeShowForm = { payload ->
// Return false to prevent the form from showing
true
},
),
)
}Prop
Type
2. Identify users
Identify the current user. The userName is required (can be a username, email, or unique identifier). Traits and options are optional.
Username format
userName must be an ASCII identifier: 1–50 characters, using only letters A–Z / a–z, digits 0–9, and ., _, @, -. Spaces and non-English characters (Unicode, accented letters, emoji, etc.) are not supported. Use an email address, internal user ID, or ASCII username — for example user@example.com or user_123. To store a display name in another language, pass it as a trait instead (e.g. set = mapOf("display_name" to JsonPrimitive("…"))).
Encatch.identifyUser("user@example.com")Trait values are kotlinx.serialization JsonElements — use JsonPrimitive for strings, numbers, and booleans:
import com.encatch.sdk.UserTraits
import kotlinx.serialization.json.JsonPrimitive
Encatch.identifyUser(
"user@example.com",
traits = UserTraits(
set = mapOf(
"name" to JsonPrimitive("Alice"),
"plan" to JsonPrimitive("team"),
),
),
)import com.encatch.sdk.UserTraits
import kotlinx.serialization.json.JsonPrimitive
Encatch.identifyUser(
"user@example.com",
traits = UserTraits(
set = mapOf("name" to JsonPrimitive("Alice"), "plan" to JsonPrimitive("team")),
setOnce = mapOf("firstSeen" to JsonPrimitive("2026-08-05T12:00:00Z")),
increment = mapOf("loginCount" to 1.0),
decrement = mapOf("credits" to 5.0),
unset = listOf("trialEndDate"),
),
)Prop
Type
User traits support the following operations:
| Operation | Type | Description |
|---|---|---|
set | Map<String, JsonElement>? | Set user attributes (overwrites existing values) |
setOnce | Map<String, JsonElement>? | Set user attributes only if they don't already exist |
increment | Map<String, Double>? | Increment numeric user attributes |
decrement | Map<String, Double>? | Decrement numeric user attributes |
unset | List<String>? | Remove user attributes |
Recommended
Using the secure option with a server-generated signature is recommended to verify that identification requests come from your backend. Keep your secret key on the server only — never expose it in client-side code.
Pass a server-generated HMAC signature so Encatch can validate the request. generatedDateTimeInUtc must be milliseconds since the Unix epoch (the string form of your server's epoch-millis timestamp). When your publishable key has a session timeout, use the same value in HMAC-SHA256(userName + epochMs, secretKey). It is sent as the X-User-Signature-Time header and limits the signature's lifespan.
import com.encatch.sdk.IdentifyOptions
import com.encatch.sdk.SecureOptions
Encatch.identifyUser(
"user@example.com",
options = IdentifyOptions(
secure = SecureOptions(
signature = "your-hmac-signature",
generatedDateTimeInUtc = "1741867200000", // ms since epoch (2025-03-13T12:00:00Z)
),
),
)3. Show a form manually
Show a specific form by slug or ID.
import com.encatch.sdk.ResetMode
import com.encatch.sdk.ShowFormOptions
Encatch.showForm("feedback-form")
Encatch.showForm("feedback-form", ShowFormOptions(reset = ResetMode.ALWAYS))Prop
Type
Prop
Type
| ResetMode | Behavior |
|---|---|
ResetMode.ALWAYS | Reset pre-fill and response data on every form display |
ResetMode.ON_COMPLETE | Reset only after the form is completed |
ResetMode.NEVER | Never reset response data |
Pass caller context when showing a form. Context values use the ContextValue sealed class (StringValue, NumberValue, BooleanValue, DateValue):
import com.encatch.sdk.ContextValue
import com.encatch.sdk.ResetMode
import com.encatch.sdk.ShowFormOptions
Encatch.showForm(
"feedback-form",
ShowFormOptions(
reset = ResetMode.ALWAYS,
context = mapOf(
"plan" to ContextValue.StringValue("team"),
"feature" to ContextValue.StringValue("checkout"),
),
),
)Other actions
Set the user's preferred language.
Encatch.setLocale("fr")Set the user's country.
Encatch.setCountry("FR") // ISO 3166 country codeSet the theme for forms and surveys.
import com.encatch.sdk.Theme
Encatch.setTheme(Theme.DARK)
Encatch.setTheme(Theme.LIGHT)
Encatch.setTheme(Theme.SYSTEM) // Follows system preferenceEncatch.trackEvent("button_clicked")Encatch.trackScreen("HomeScreen")Subscribe to form lifecycle events. on returns an unsubscribe function — there is no separate off in the KMP API; call the returned function instead.
val unsubscribe = Encatch.on { eventType, payload ->
println("Event: ${eventType.wireValue}, formId: ${payload.formId}")
}
// Later, to unsubscribe:
unsubscribe()| Event | Description |
|---|---|
EventType.FORM_SHOW | Fired when a form is displayed |
EventType.FORM_STARTED | Fired when a user starts interacting |
EventType.FORM_SUBMIT | Fired when a form is submitted |
EventType.FORM_COMPLETE | Fired when a form is fully completed |
EventType.FORM_CLOSE | Fired when a form is closed |
EventType.FORM_DISMISSED | Fired when a form is dismissed without completion |
EventType.FORM_ERROR | Fired when an error occurs |
EventType.FORM_SECTION_CHANGE | Fired when the visible section changes |
EventType.FORM_ANSWERED | Fired when a question is answered |
EventType.FORM_REMIND_ME_LATER | Fired when the user taps "Remind me later" |
EventType.FORM_CTA_TRIGGERED | Fired when a completion CTA is triggered on thank-you or exit screens |
Handle completion CTAs (in-app navigation, internal redirect, or external browser) via FORM_CTA_TRIGGERED. Configure actions in the form builder — see Call to action. The SDK closes the form overlay after emitting the event — your app handles in-app navigation. Event payload data is a Map<String, JsonElement>:
import com.encatch.sdk.EventType
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
Encatch.on { eventType, payload ->
if (eventType != EventType.FORM_CTA_TRIGGERED) return@on
val action = (payload.data?.get("action") as? JsonPrimitive)?.contentOrNull
if (action != "app_navigate") return@on
val route = (payload.data?.get("route") as? JsonPrimitive)?.contentOrNull
// Map route strings to your app's navigation paths
when (route) {
"billing", "billing/upgrade" -> navigateTo("/billing")
}
}For in-app navigation, the SDK closes the form overlay after emitting the event and expects your app to perform navigation. For URL redirect actions, the SDK opens the URL and closes the form automatically.
Pre-fill a form response before showing a form. questionId may be a question UUID or a question slug.
Encatch.addToResponse("question_id", "pre-filled value")
Encatch.addToResponse("choice_question_id", listOf("option-a", "option-b"))
Encatch.showForm("your-form-slug")Inspect or clear pending pre-fills:
val pending: Map<String, Any?> = Encatch.getPendingResponses()
Encatch.clearPendingResponses()Dismiss the currently displayed form.
Encatch.dismissForm()
// Or dismiss a specific form configuration:
Encatch.dismissForm(formConfigurationId = "config-id")Use onBeforeShowForm in EncatchConfig to conditionally block forms from showing. The interceptor is a suspend lambda, so it may await anything — user input, a coroutine, a network check — before answering.
import com.encatch.sdk.EncatchConfig
import com.encatch.sdk.TriggerType
Encatch.init(
"your-api-key",
EncatchConfig(
onBeforeShowForm = { payload ->
// Inspect payload.formId, payload.triggerType, payload.formConfigJson, etc.
if (payload.triggerType == TriggerType.AUTOMATIC && someCondition) {
false // Block this form
} else {
true // Allow
}
},
),
)Register a debug hook that receives every completed SDK HTTP call (request + response). Only fires when EncatchConfig.debugMode is enabled; the API key header is always masked to its last 5 characters. Assignment-style (last caller wins) and survives re-init() — set it once at app startup for in-app network inspectors.
Encatch.setOnNetworkLog { entry ->
println("${entry.method} ${entry.endpoint} -> ${entry.status} in ${entry.durationMs}ms")
}
// Pass null to clear:
Encatch.setOnNetworkLog(null)Control session lifecycle manually:
import com.encatch.sdk.StartSessionOptions
Encatch.startSession()
// Skip the immediate ping or screen re-track on start:
Encatch.startSession(
StartSessionOptions(
skipImmediatePing = true,
skipImmediateTrackScreen = true,
),
)// Temporarily stop the 30-second background ping (not persisted)
Encatch.pauseSession()
// Resume the ping interval after pauseSession()
Encatch.resumeSession()// Fully suspend SDK activity — stops ping and dismisses open forms.
// Persists across app restarts. Re-enable with startSession().
Encatch.stopSession()Reset the current user identity and clear persisted identity data. Reverts the SDK to anonymous mode. User identity is preserved across stopSession() — use resetUser() after logout.
Encatch.resetUser()Wipes all persisted SDK data and resets in-memory state. Stronger than resetUser() — also clears session-stopped state and device preferences. The SDK remains initialized; call identifyUser afterward.
Encatch.clearAll()The SDK sends a background ping every 30 seconds (configurable via server response) to maintain engagement sessions and check for triggered forms. Ping is suppressed while a form is visible.
Inline Forms
Inline forms are a way to show Encatch in-app feedback without a modal — the survey renders directly in your layout instead of as a full-screen overlay.
No UI layer in this module
com.encatch:kmp-sdk ships no views or composables. Inline forms are rendered by embedding the platform-native inline form view in each platform's UI code. If you use Compose Multiplatform, prefer the Compose Multiplatform SDK, which wraps both native views in a single EncatchInlineForm composable you call from commonMain.
Embed the native inline view directly in your platform UI code. Routing (exact formId match, wildcard, modal fallback) is resolved by the platform SDK the same way on both platforms:
- Exact match — an inline view whose
formIdmatches theshowFormpayload wins. - Wildcard — an inline view with no
formIdcatches anything not exact-matched. - Modal fallback — the modal overlay presents the form when no inline slot is registered or none match.
Use com.encatch.android.EncatchInlineFormView from the underlying native Android SDK — it is available on your classpath transitively:
import com.encatch.android.EncatchInlineFormView
// In your Activity/Fragment or view code:
val inlineForm = EncatchInlineFormView(context).apply {
formId = "your-form-slug" // exact match; null = wildcard
}
container.addView(inlineForm)Then trigger the form from shared code:
Encatch.showForm("your-form-slug")Use EncatchInlineFormView from the native Swift iOS SDK in your SwiftUI/UIKit host code:
import Encatch
// UIKit:
let inlineForm = EncatchInlineFormView()
inlineForm.formId = "your-form-slug" // exact match; nil = wildcard
stackView.addArrangedSubview(inlineForm)Then trigger the form from shared code:
Encatch.showForm("your-form-slug")The KMP module does not yet expose a commonMain accessor for the inline view type itself (a known gap) — the views above are used from each platform's own UI layer, while all business-logic calls stay in commonMain.
Build Your Own Form UX & UI
If your feedback flow uses a fixed, predictable question set — the same fields and workflow every time — you can build the form with your own native UI and submit responses through the SDK. That keeps typography, spacing, colors, and interaction patterns aligned with the rest of your app, so the survey feels like a native screen rather than an embedded web page.
The flow has three parts, all available from commonMain:
- Intercept the form with
onBeforeShowFormand returnfalse. TheShowFormInterceptorPayloadincludesformConfigJson— the JSON encoding of the full form configuration (includingquestionnaireFields), so you can render your own UI from the real form definition. - Render your own UI from the payload.
- Submit with
buildSubmitRequest+Encatch.submitForm.
import com.encatch.sdk.BuildSubmitRequestOptions
import com.encatch.sdk.Encatch
import com.encatch.sdk.EncatchConfig
import com.encatch.sdk.NativeFormResponse
import com.encatch.sdk.buildSubmitRequest
// 1. Intercept: block the SDK's own rendering for this form
Encatch.init(
"your-api-key",
EncatchConfig(
onBeforeShowForm = { payload ->
if (payload.formId == "my-native-form") {
showMyNativeForm(payload.formId, payload.formConfigJson)
false // block the SDK form — we render our own
} else {
true
}
},
),
)
// 3. Submit: convert your native answers and post them to Encatch
suspend fun submitMyNativeForm(formConfigurationId: String) {
val responses = listOf(
NativeFormResponse("q1", "rating", 5),
NativeFormResponse("q2", "short_answer", "Great product!"),
NativeFormResponse("q3", "multiple_choice_multiple", listOf("option-a", "option-b")),
)
val requestJson = buildSubmitRequest(
BuildSubmitRequestOptions(formConfigurationId = formConfigurationId),
responses,
)
Encatch.submitForm(requestJson)
}NativeFormResponse.value's expected shape depends on the question type: numeric scales (rating, nps, csat, opinion_scale) take a Number or numeric String; text types take String; choice and ranking types take String or List<String>; boolean types (yes_no, consent) take Boolean. All 33 Encatch question types are supported; unknown types fall back to short_answer for forward-compatibility.
Prop
Type
| Member | Description |
|---|---|
init(apiKey, config) | Initialize the SDK (suspend) |
isInitialized | Whether init has completed |
identifyUser(userName, traits, options) | Identify a user (suspend) |
setLocale(locale) | Set locale |
setCountry(country) | Set country (ISO 3166) |
setTheme(theme) | Set form theme |
trackEvent(eventName) | Track a custom event (suspend) |
trackScreen(screenName) | Track screen navigation (suspend) |
showForm(formId, options) | Show a form (inline or modal) (suspend) |
dismissForm(formConfigurationId) | Dismiss the current form (suspend) |
addToResponse(questionId, value) | Pre-fill a question answer |
getPendingResponses() | Read pending pre-fills |
clearPendingResponses() | Clear pending pre-fills |
submitForm(requestJson) | Submit a custom native form (suspend) |
startSession(options) | Start a new session (suspend) |
pauseSession() / resumeSession() | Pause / resume background ping |
stopSession() | Suspend SDK activity (suspend) |
resetUser() | Reset user identity (suspend) |
clearAll() | Wipe all persisted SDK data (suspend) |
on(callback) | Subscribe to lifecycle events; returns unsubscribe function |
emitEvent(eventType, payload) | Emit an event to listeners |
setOnNetworkLog(callback) | Debug hook for SDK HTTP calls (debugMode only) |
stop() | Teardown |
Read-only getters: apiKey, baseUrl, webHost, isFullScreen, theme, locale, deviceId, sessionId, userName, userId, debugMode.
- Compose Multiplatform SDK — this module plus an
EncatchInlineFormcomposable; the form host installs itself, so there is no per-platform setup. - Android SDK — the native Android SDK this module forwards to on Android.
- iOS SDK — the native Swift SDK this module forwards to on iOS.
Support
- Maven Central:
com.encatch:kmp-sdk - Issues: github.com/get-encatch/encatch-android/issues
Was this page helpful?
