Documentation

Instant Journey presentation on Android

Prepare a Journey before it appears in Android View and Jetpack Compose apps.

Prepare a published Journey while the preceding screen is visible, then present it without waiting for a configuration request or a new renderer. FounderHQ retains one renderer for each host and keeps preparation free of impressions, answers, and completion events.

Before you start

Install version 0.8.1 or later. Preparation arrived in 0.7.0. Existing load(...) calls keep working: they prepare and present in one operation. Adopt the persistent host APIs below when presentation latency matters.

Install

Add mavenCentral() to your dependency repositories, then install the core View SDK and, for Compose apps, its adapter:

implementation("com.getfounderhq:journeys:0.8.1")
implementation("com.getfounderhq:journeys-compose:0.8.1")

The SDK supports Android API 24 and later.

Jetpack Compose

Place one JourneyHost at a stable point in the composition. Keep it composed while navigation changes the surrounding screen.

@Composable
fun OnboardingEntry() {
    val journeyState = rememberJourneyState()

    Box(Modifier.fillMaxSize()) {
        Button(onClick = journeyState::present) {
            Text("Start onboarding")
        }

        JourneyHost(
            configuration = JourneyConfiguration(
                apiKey = "fhq_pk_XXXX",
                journeyId = "journey_123",
                identity = JourneyIdentity(externalId = "usr_1042"),
            ),
            state = journeyState,
            modifier = Modifier.fillMaxSize(),
            listener = object : JourneyListener {
                override fun onEvent(event: JourneyEvent) {
                    if (event.type == JourneyEventType.COMPLETE) {
                        journeyState.dismiss()
                    }
                }
            },
        )
    }
}

JourneyHost begins preparation when it enters the composition. Observe journeyState.readiness or journeyState.isPrepared when the surrounding UI needs to reflect readiness. present() shows the prepared renderer. dismiss() hides it and resets that same renderer with a fresh client session, so the next presentation does not show completed steps or previous answers.

Call journeyState.prepare() to retry a failed preparation or ask the retained host to refresh. Call journeyState.dispose() only when the owning flow is finished; otherwise keep the host composed and use dismiss().

Android Views

Keep one JourneyView attached to the host Activity or Fragment and use the same lifecycle directly:

val journeyView = JourneyView(requireContext())

journeyView.prepare(
    JourneyConfiguration(
        apiKey = "fhq_pk_XXXX",
        journeyId = "journey_123",
        identity = JourneyIdentity(externalId = "usr_1042"),
    ),
    listener,
)

startButton.setOnClickListener {
    journeyView.present()
}

closeButton.setOnClickListener {
    journeyView.dismiss()
}

Call dispose() when the host is permanently destroyed. The View also cleans up with its lifecycle owner and releases a hidden prepared renderer under memory pressure. Changing identity invalidates the renderer before another Journey can start.

Readiness, refresh, and retries

Preparation fetches the published configuration while the shared renderer loads in parallel. The server controls the refresh interval, which the SDK clamps to 10–30 minutes. A successfully prepared configuration can start for up to 30 minutes. A failed refresh does not extend that window; the SDK retries only after its backoff when the user presents again, explicitly retries, or the app next enters the foreground. It does not run a repeated network retry loop. HTTP 429 responses honor Retry-After, and manual retry stays disabled until that delay has passed.

The configuration and revision stay fixed while a Journey is visible. A newly published revision becomes the next prepared presentation after the active one is dismissed. Capture batches keep the revision, client session, identity, and queue metadata assigned when each event was created, including batches sent after an app restart.

An authorization denial immediately ends interaction, discards prepared state, and suppresses automatic requests. Try again or an explicit prepare() / present() performs a fresh authorization request with the same configuration, so a republished Journey or restored entitlement can recover without rebuilding its host.

Renderers that do not advertise preparation support use the compatible direct path. They initialize only after present() and never emit hidden analytics.

Loading and error UI

The default presentation shows an accessible, theme-aware progress ring without text. Reduced motion uses a static ring. If the Journey is not rendered within 15 seconds of foreground time, the user sees a restrained status icon, Unable to load. Please try again., and an explicit Try again button. The button stays disabled only while a request is active or a server-directed backoff is pending.

View apps can replace both surfaces:

journeyView.loadingViewFactory = { context -> MyLoadingView(context) }
journeyView.errorViewFactory = { context, error, retry ->
    MyJourneyErrorView(context).apply {
        setRetryAction(retry)
    }
}

Compose apps set these factories through JourneyHost(configureView = { ... }). Keep custom loading and retry controls accessible, and call the supplied retry function instead of creating a second host.

AI agent or LLM? Read this page as markdown

On this page