Optimize flow & paywall fetching in Android SDK

A reliable flow or paywall fetch on Android does three things: renders fast, returns the audience-targeted variation, and falls back gracefully when the network is slow. The rules below cover the timing, caching, and fallback patterns to get there.

Tip

The rules assume Adapty.activate() and Adapty.identify() have already resolved. See Call order in Android SDK.

Rules and pitfalls

Do thisDon’t do thisWhy
Fetch the placement you’re about to show, or warm the cache with preloadFlows (SDK 4.1+).Fan out your own concurrent getFlow calls on launch.A hand-rolled prefetch burst blocks the main thread and produces a black screen. preloadFlows is built for this and runs the batch concurrently for you.
Fetch getFlow after attribution has had a chance to resolve — for example, 1–2 seconds after activate or after setOnProfileUpdatedListener fires.Call getFlow in Application.onCreate().Attribution hasn’t landed yet. The flow resolves against the default audience and silently bypasses segments and ASA personalization.
Set a loadTimeout and configure a fallback paywall for every placement.Wait on getFlow indefinitely.Without a timeout, users on poor connectivity see a blank screen until the network resolves — or close the app.

See Fetch paywalls and products for the fetchPolicy and loadTimeout parameter reference, and Placements for picking the right placement.

Preload placements

Info

preloadFlows and preloadFlowsForDefaultAudience are available starting from SDK version 4.1.

preloadFlows caches the flow JSON ahead of time — one request per placement. You then use it as usual: getFlow for the flow, getFlowConfiguration for its view configuration.

fetchPolicy decides which layer a later getFlow reads first, not whether it can reach the cache at all:

  • ReturnCacheDataElseLoad reads the preloaded copy first and goes to the network only when nothing is cached. ReturnCacheDataIfNotExpiredElseLoad(maxAgeMillis) does the same while the copy is younger than maxAgeMillis.
  • The default, ReloadRevalidatingCacheData, goes to the network first and falls back to the preloaded copy when the request fails or times out.

A preload pays off either way, but differently: a cache-first policy removes the request, while the default keeps it and gains a warm copy to fall back on.

Use it when you know which placements the session will need but don’t want to show them yet — for example, right after activate and identify resolve, for the flow behind a button the user hasn’t tapped.

Parameters:

  • placementIds (required): the placements to preload. Blank and duplicate IDs are ignored.
  • loadTimeout (optional): timeout applied to each placement in the batch, not to the batch as a whole. Defaults to 5 seconds, and values below 1 second are raised to 1 second.

Behavior worth knowing:

  • The callback fires only after every placement has been attempted, and it reports the per-placement failures together. A failure for one placement doesn’t stop the others.
  • If a placement times out, hits a server error, or fails with a network error, the SDK falls back to the fallback variations for that placement. Other failures are reported as-is.
  • Preloading only warms the cache. It doesn’t return content — you still call getFlow to display it.

What a preload covers

A flow reaches the screen in layers. A preload covers the first, exactly as getFlow does:

LayerFetched byWarmed by a preload
Flow JSON — the chosen variation, its product IDs, and the remote configgetFlowYes
UI layout — the screen’s structure, styling, and textgetFlowConfigurationNo
Images, including the still frame that stands in for a video elementgetFlowConfiguration, in the backgroundNo
Video filesThe system player, as the screen rendersNot cached by the SDK

getFlowConfiguration awaits the layout, so the first request for a given layout costs a round trip even after a preload. The SDK then holds that layout in its own disk cache, which survives app restarts and is read before any network call, so the cost falls on the first request rather than on every one. Once the SDK has the layout, it starts caching the images independently of the call: it doesn’t hold up the screen, and there’s no callback or error that reports when it finishes.

Find out which placement failed

The callback receives an AdaptyPreloadPlacementsError covering the whole batch, with the code REQUEST_FAILED (2005). To see the individual failures, read its preloadErrors property — a map keyed by placement ID:

Adapty.preloadFlows(listOf("onboarding", "main_paywall")) { error ->
    if (error is AdaptyPreloadPlacementsError) {
        error.preloadErrors.forEach { (placementId, placementError) ->
            // log or retry the individual placement
        }
    }
}

preloadErrors exists only on AdaptyPreloadPlacementsError, so confirm the type first — any other AdaptyError came from something other than a per-placement failure.

Skip audience segmentation

To warm the cache without waiting for audience segmentation at all, use the default-audience variant. It takes no loadTimeout:

Adapty.preloadFlowsForDefaultAudience(listOf("main_paywall")) { error -> }

Show first-screen media from the app bundle

A flow downloads its images and videos from Adapty. To show the first screen’s media instantly, serve it from the app bundle instead. This is a good way to reuse media you already ship, such as the visuals of an existing native onboarding.

  1. In the Flow & Paywall Builder, set a custom media ID on the image or video. The file you upload there stays as the fallback.
  2. Add the file to your app’s res/raw or assets folder.
  3. When you create the flow view with getFlowView, pass the bundled file for that ID in customAssets:
// "welcome_video" is the custom media ID set in the Flow & Paywall Builder
val bundledAssets = AdaptyCustomAssets.of(
    "welcome_video" to
            AdaptyCustomVideoAsset.file(
                FileLocation.fromResId(requireContext(), R.raw.welcome),
                preview = AdaptyCustomImageAsset.file(
                    FileLocation.fromResId(requireContext(), R.drawable.welcome_poster),
                ),
                resolution = AdaptyCustomVideoAsset.Resolution(width = 1080, height = 1920),
            ),
)

val flowView = AdaptyUI.getFlowView(
    activity,
    flowConfiguration,
    products,
    eventListener,
    insets,
    bundledAssets,
)

Bundled files add to your app’s download size, so bundle only the media users see first.

Media you don’t bundle still appears right away: the view configuration carries a small low-resolution copy of each image, including a video’s still frame, and shows it until the full file loads.

For the full customAssets reference, see Customize assets.

Tune for poor connectivity

For markets with consistently poor connectivity (rural areas, transit, regions affected by routing):

  • Set fetchPolicy to AdaptyPlacementFetchPolicy.ReturnCacheDataElseLoad on every fetch except the very first.
  • Configure a fallback paywall for every placement in the Adapty dashboard.
  • Set loadTimeout to 3–5 seconds and accept the fallback when the timeout fires.
  • Don’t gate flow display on getProfile. Call getFlow independently so a slow profile doesn’t block the UI.