The AppDNA SDK supports server-driven onboarding flows that are configured remotely and rendered natively using Jetpack Compose. Onboarding flows guide users through welcome screens, questions, and value propositions without requiring app updates.
Present an Onboarding Flow
Present an onboarding flow by passing the current Activity and an optional flow ID:
import ai.appdna.sdk.AppDNA
val presented = AppDNA.presentOnboarding(
activity = this,
flowId = "main_flow",
listener = onboardingListener
)
if (!presented) {
Log.w("Onboarding", "No onboarding flow available")
}
The method returns a Boolean indicating whether the flow was successfully presented. If flowId is null, the SDK uses the active flow from the remote configuration.
// Use the default active flow
val presented = AppDNA.presentOnboarding(
activity = this,
flowId = null,
listener = onboardingListener
)
Onboarding Module
The AppDNA.onboarding module provides direct access to onboarding functionality:
| Method | Return | Description |
|---|
present(activity, flowId?, context?) | Boolean | Presents the onboarding flow |
setDelegate(delegate: AppDNAOnboardingDelegate?) | Unit | Sets the onboarding delegate |
Onboarding Context
Pass additional context when presenting an onboarding flow:
import ai.appdna.sdk.onboarding.OnboardingContext
val context = OnboardingContext(
source = "deep_link",
campaign = "summer_promo",
referrer = "friend_invite",
userProperties = mapOf("tier" to "free"),
experimentOverrides = mapOf("welcome_variant" to "b")
)
AppDNA.onboarding.present(
activity = this,
flowId = "main_flow",
context = context
)
OnboardingContext Properties
| Property | Type | Description |
|---|
source | String? | Where the user came from |
campaign | String? | Marketing campaign identifier |
referrer | String? | Referral source |
userProperties | Map<String, Any>? | Additional user properties for targeting |
experimentOverrides | Map<String, String>? | Override experiment variants for testing |
Onboarding Delegate
Implement AppDNAOnboardingDelegate to receive onboarding lifecycle callbacks:
import ai.appdna.sdk.onboarding.AppDNAOnboardingDelegate
class MyOnboardingDelegate : AppDNAOnboardingDelegate {
override fun onOnboardingStarted(flowId: String) {
Log.d("Onboarding", "Flow started: $flowId")
}
override fun onOnboardingStepChanged(
flowId: String,
stepId: String,
stepIndex: Int,
totalSteps: Int
) {
Log.d("Onboarding", "Step $stepIndex/$totalSteps: $stepId")
}
override fun onOnboardingCompleted(flowId: String, responses: Map<String, Any>) {
Log.d("Onboarding", "Flow completed: $flowId")
Log.d("Onboarding", "Responses: $responses")
// Navigate to main app
}
override fun onOnboardingDismissed(flowId: String, atStep: Int) {
Log.d("Onboarding", "Flow dismissed at step $atStep")
}
// Async hooks (optional — default implementations provided)
override suspend fun onBeforeStepAdvance(
flowId: String, fromStepId: String, stepIndex: Int,
stepType: String, responses: Map<String, Any>,
stepData: Map<String, Any>?
): StepAdvanceResult = StepAdvanceResult.Proceed
override suspend fun onBeforeStepRender(
flowId: String, stepId: String, stepIndex: Int,
stepType: String, responses: Map<String, Any>
): StepConfigOverride? = null
}
// Set the delegate
AppDNA.onboarding.setDelegate(MyOnboardingDelegate())
Step Types
Onboarding flows are composed of steps, each with a specific type:
| Step Type | Description |
|---|
WELCOME | Welcome or splash screen |
QUESTION | User input step with single or multi-select options |
VALUE_PROP | Value proposition or feature highlight screen |
FORM | Structured form with multiple native input fields |
PERMISSION | Push notification or tracking permission request |
CUSTOM | Custom step rendered from server-defined templates |
Selection Modes
Question steps support two selection modes:
| Mode | Description |
|---|
SINGLE | User selects exactly one option |
MULTI | User can select multiple options |
Onboarding flows are rendered using Jetpack Compose. The SDK handles the full UI lifecycle, including step transitions, animations, and response collection. Your app only needs to handle the delegate callbacks.
Content Blocks
Each onboarding step is composed of content blocks that control the visual layout. Blocks are configured in the Console and rendered natively using Jetpack Compose. The following block types are available:
| Block Type | Description |
|---|
TITLE | Primary heading text |
SUBTITLE | Secondary descriptive text |
IMAGE | Static image with sizing and corner radius |
LOTTIE | Lottie animation (JSON or dotLottie) |
RIVE | Rive state-machine animation |
VIDEO | Inline video (MP4, HLS) with autoplay and loop options |
BUTTON | Tappable button with configurable action |
OPTION_LIST | List of selectable options (single or multi-select) |
FORM | Form input group (see Form Steps below) |
PAGE_INDICATOR | Dot or bar indicator showing current step progress |
WHEEL_PICKER | Scrollable wheel-style picker for value selection |
PULSING_AVATAR | Animated avatar with a pulsing ring effect |
SOCIAL_LOGIN | Social sign-in buttons (Google, etc.) |
TIMELINE | Vertical timeline with labeled milestones |
ANIMATED_LOADING | Skeleton or spinner loading animation between steps |
COUNTDOWN_TIMER | Countdown timer with configurable duration and expiry action |
RATING | Star or emoji rating selector |
RICH_TEXT | Markdown-style rich text with inline formatting |
PROGRESS_BAR | Horizontal progress bar with percentage or label |
CIRCULAR_GAUGE | Circular progress indicator with value label |
DATE_WHEEL_PICKER | Native date wheel picker (day/month/year columns) |
STACK | Vertical container that groups child blocks |
ROW | Horizontal container that arranges child blocks side by side |
CUSTOM_VIEW | Host-app-provided Composable view (see Custom View Registration) |
STAR_BACKGROUND | Animated starfield or particle background effect |
PRICING_CARD | Product pricing card with plan details and CTA |
Blocks are configured entirely in the Console. The SDK renders them automatically — no code is needed unless you register custom views.
Custom View Registration
Register your own Composable views to be rendered inside onboarding steps wherever a CUSTOM_VIEW block appears:
AppDNA.registerCustomView("my_view") {
MyComposableView()
}
// Register before presenting the onboarding flow
AppDNA.registerCustomView("terms_acceptance") {
TermsAcceptanceView(onAccept = { accepted ->
// Handle acceptance
})
}
The id must match the custom view identifier configured in the Console for the CUSTOM_VIEW block.
Dynamic Bindings
When you return data from the onBeforeStepRender hook, that data flows into block properties via a bindings map. This allows you to inject dynamic content into any block at render time.
override suspend fun onBeforeStepRender(
flowId: String, stepId: String, stepIndex: Int,
stepType: String, responses: Map<String, Any>
): StepConfigOverride? {
return StepConfigOverride(
bindings = mapOf(
"hero_title" to "Welcome, $userName!",
"plan_price" to fetchedPrice,
"avatar_url" to userAvatarUrl
)
)
}
In the Console, reference bindings in block properties using {{binding_key}} syntax. The SDK resolves these before rendering.
Block Styling
Every content block supports a block_style design token that controls appearance properties such as padding, margin, background color, corner radius, border, shadow, and opacity. Block styles are configured in the Console and applied automatically by the SDK.
Visibility Conditions
Blocks can be shown or hidden based on user responses, bindings, or device attributes. Visibility conditions are configured per-block in the Console using rules like answer_equals, binding_not_empty, platform_is, and locale_matches. The SDK evaluates conditions client-side before rendering each block.
Entrance Animations
Each block supports an entrance animation that plays when the block first appears. Animations are configured per-block in the Console. Supported animation types include FADE, SLIDE_UP, SLIDE_DOWN, SLIDE_LEFT, SLIDE_RIGHT, SCALE, FLIP, and BOUNCE. You can configure duration, delay, and easing curve.
The FORM step type provides native input fields for collecting structured user data. Each form can contain multiple fields with validation, conditional visibility, and custom configuration.
| Type | Description | Example Use Case |
|---|
TEXT | Single-line text input | Name, username |
TEXTAREA | Multi-line text input | Bio, notes |
NUMBER | Numeric input with stepper | Age, quantity |
EMAIL | Email input with validation | Email address |
PHONE | Phone number input | Contact number |
DATE | Date picker | Birthday, start date |
TIME | Time picker | Preferred time |
DATETIME | Combined date and time picker | Appointment scheduling |
SELECT | Dropdown or scrollable picker | Country, category |
SLIDER | Numeric slider with min/max | Budget, intensity level |
TOGGLE | On/off switch | Opt-in preferences |
STEPPER | Increment/decrement counter | Number of items |
SEGMENTED | Segmented control for few options | Gender, frequency |
PASSWORD | Secure text input with visibility toggle | Password, PIN |
RATING | Star rating input | Satisfaction, preference |
RANGE_SLIDER | Dual-handle range slider | Price range, age range |
IMAGE_PICKER | Photo picker from gallery or camera | Profile photo, document |
COLOR | Color picker or preset swatches | Theme preference, branding |
URL | URL input with validation | Website, portfolio link |
CHIPS | Tag-style multi-select chips | Interests, skills |
SIGNATURE | Freehand signature drawing pad | Agreement, consent |
LOCATION | Autocomplete location search | City, address, country |
Field Validation
Form fields support built-in and custom validation:
- Required fields — marked in the Console, the SDK prevents advancing until filled
- Regex patterns — custom validation (e.g.,
^[A-Z]{2}\\d{4}$ for a code format)
- Min/max values — for
NUMBER, SLIDER, and STEPPER fields
- Max length — for
TEXT and TEXTAREA fields
Conditional Fields
Fields can depend on other fields using depends_on rules. For example, a “Company name” field can appear only when the user selects “Employed” in a previous field. Supported operators: equals, not_equals, contains, not_empty, empty, gt, lt, is_set.
Form field values are included in the responses map passed to onOnboardingCompleted, keyed by step ID:
override fun onOnboardingCompleted(flowId: String, responses: Map<String, Any>) {
val formData = responses["profile_step"] as? Map<String, Any>
val name = formData?.get("full_name") as? String
val age = formData?.get("age") as? Int
val email = formData?.get("email") as? String
// Use collected data to personalize the experience
}
Location Fields
The LOCATION field type provides an autocomplete search input that returns structured location data including city, state, country, coordinates, and timezone.
When the user types a location name, the SDK calls the AppDNA geocoding proxy and displays matching suggestions. The selected result is stored as structured data.
Location Data Structure
Each location selection contains:
| Field | Type | Example |
|---|
formatted_address | String | "New York, New York, United States" |
city | String | "New York" |
state | String | "New York" |
state_code | String | "NY" |
country | String | "United States" |
country_code | String | "US" |
latitude | Double | 40.7128 |
longitude | Double | -74.0060 |
timezone | String | "America/New_York" |
timezone_offset | Int | -300 (minutes from UTC) |
postal_code | String? | null |
Accessing Location Data
Location data is included in the form responses:
override fun onOnboardingCompleted(flowId: String, responses: Map<String, Any>) {
val locationStep = responses["location_step"] as? Map<String, Any>
val location = locationStep?.get("user_location") as? Map<String, Any>
if (location != null) {
val city = location["city"] as? String // "New York"
val country = location["country_code"] as? String // "US"
val timezone = location["timezone"] as? String // "America/New_York"
val lat = location["latitude"] as? Double // 40.7128
val lng = location["longitude"] as? Double // -74.0060
}
}
Template Engine
Location data is accessible in dynamic content templates:
Welcome from {{onboarding.location_step.user_location.city}}!
Your timezone: {{onboarding.location_step.user_location.timezone}}
Configuration in Console
In the onboarding flow editor, add a LOCATION field to a form step and configure:
| Option | Description | Default |
|---|
| Location Type | Filter results: city, address, region, country | city |
| Bias Country | ISO country code to prioritize results (e.g., US) | None |
| Language | Language for results (e.g., en, fr) | en |
| Min Characters | Characters required before search triggers | 2 |
Location autocomplete uses a server-side proxy — no third-party SDK is added to your app binary. The geocoding provider (Mapbox by default) can be configured in Settings > Geocoding.
Async Step Hooks
The onboarding delegate supports two suspend functions that let you intercept step transitions for server-side validation, dynamic content loading, or custom routing logic.
onBeforeStepAdvance
Called before the SDK advances to the next step. Return a StepAdvanceResult to control what happens next:
override suspend fun onBeforeStepAdvance(
flowId: String,
fromStepId: String,
stepIndex: Int,
stepType: String,
responses: Map<String, Any>,
stepData: Map<String, Any>?
): StepAdvanceResult {
// Example: validate a referral code with your backend
if (stepType == "form") {
val formData = responses[fromStepId] as? Map<String, Any>
val code = formData?.get("referral_code") as? String
if (code != null) {
val isValid = validateReferralCode(code) // your suspend function
if (!isValid) {
return StepAdvanceResult.Block("Invalid referral code. Please try again.")
}
return StepAdvanceResult.ProceedWithData(mapOf("referral_validated" to true))
}
}
return StepAdvanceResult.Proceed
}
StepAdvanceResult
| Case | Description |
|---|
Proceed | Continue to the next step normally |
ProceedWithData(data) | Continue and merge additional data into the session |
Block(message) | Block advancement and show an error message to the user |
SkipTo(stepId, data?) | Skip to a specific step by ID, optionally with data |
onBeforeStepRender
Called before a step is rendered. Return a StepConfigOverride to dynamically modify the step’s content:
override suspend fun onBeforeStepRender(
flowId: String,
stepId: String,
stepIndex: Int,
stepType: String,
responses: Map<String, Any>
): StepConfigOverride? {
// Pre-fill form fields based on user data
if (stepId == "profile_step") {
return StepConfigOverride(
fieldDefaults = mapOf(
"email" to currentUser.email,
"name" to currentUser.displayName
),
title = "Welcome back, ${currentUser.firstName}!"
)
}
return null
}
Both hooks are suspend functions — the SDK shows a loading indicator while waiting for your response. If either hook throws an exception or times out, the SDK proceeds normally.
Row Direction and Distribution
The ROW content block supports configurable direction and distribution, set in the Console:
| Property | Options | Description |
|---|
direction | HORIZONTAL, VERTICAL | Axis along which child blocks are arranged |
distribution | EQUAL, FILL, START, CENTER, END, SPACE_BETWEEN, SPACE_AROUND | How child blocks are distributed within the row |
// Example: Two buttons side by side, equally spaced
Row (direction: HORIZONTAL, distribution: EQUAL)
├── Button "Skip"
└── Button "Continue"
Buttons support gradient backgrounds configured in the Console:
| Property | Type | Description |
|---|
gradient_colors | List<String> | Array of hex color stops (e.g., ["#FF6B6B", "#4ECDC4"]) |
gradient_direction | String | horizontal, vertical, diagonal_tl_br, diagonal_tr_bl |
Gradients override the solid background_color when set.
Select Display Styles
Question steps with selectable options support three display styles:
| Style | Description |
|---|
DROPDOWN | Native dropdown picker. Best for long option lists (5+ items). |
STACKED | Vertically stacked option buttons. Default style. |
GRID | Grid layout with 2 or 3 columns. Good for visual options with icons. |
The display style is configured per question step in the Console.
Progress Bar Custom Colors
The PROGRESS_BAR content block supports custom color configuration:
| Property | Type | Description |
|---|
fill_color | String | Hex color for the filled portion |
track_color | String | Hex color for the unfilled track |
fill_gradient | List<String> | Gradient color stops for the fill (overrides fill_color) |
corner_radius | Number | Corner radius of the progress bar |
height | Number | Height of the progress bar in dp |
Colors are configured per block in the Console. When not set, the SDK uses the app’s primary theme color.
Per-Step Progress Visibility
The progress indicator can be hidden on specific steps while still counting them in the total progress. This is useful for splash screens, permission prompts, or transition steps where the progress bar would be distracting.
Configure hide_progress per step in the Console under Step Design > Logic. When enabled, the progress bar is hidden on that step but the step still contributes to the overall progress calculation (e.g., step 3 of 5 still advances progress to 60%).
| Property | Type | Default | Description |
|---|
hide_progress | Boolean | false | Hides the progress indicator on this step only |
This is a per-step override of the flow-level show_progress setting. If show_progress is false at the flow level, the progress bar is hidden on all steps regardless of hide_progress.
Conditional Branching
Onboarding flows support conditional branching — the next step changes based on the user’s answer. Branching is configured entirely in the Console:
- Open your flow in the Console (Onboarding > Flows)
- On a question step, click Add branching rule
- Map each answer option to a target step
The SDK handles routing automatically — no code needed. All answers are returned in the responses map in onOnboardingCompleted.
Auto-Tracked Events
The onboarding module automatically tracks the following events:
| Event | Triggered When |
|---|
onboarding_flow_started | User begins an onboarding flow |
onboarding_step_viewed | A step is displayed to the user |
onboarding_step_completed | User completes a step |
onboarding_step_skipped | User skips a step |
onboarding_flow_completed | User reaches the end of the flow |
onboarding_flow_dismissed | User dismisses the flow before completing |
The responses map passed to onOnboardingCompleted contains all user answers keyed by step ID. Use these responses to personalize the user experience or send them to your backend for analysis.
Next Steps
- Present Paywalls at the end of onboarding flows
- Set up Billing to handle purchases triggered from onboarding
- Learn about Offline Support for onboarding config caching