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.
The rules assume Adapty.activate() and Adapty.identify() have already resolved. See Call order in Android SDK.
Rules and pitfalls
| Do this | Don’t do this | Why |
|---|---|---|
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
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:
ReturnCacheDataElseLoadreads the preloaded copy first and goes to the network only when nothing is cached.ReturnCacheDataIfNotExpiredElseLoad(maxAgeMillis)does the same while the copy is younger thanmaxAgeMillis.- 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
getFlowto display it.
What a preload covers
A flow reaches the screen in layers. A preload covers the first, exactly as getFlow does:
| Layer | Fetched by | Warmed by a preload |
|---|---|---|
| Flow JSON — the chosen variation, its product IDs, and the remote config | getFlow | Yes |
| UI layout — the screen’s structure, styling, and text | getFlowConfiguration | No |
| Images, including the still frame that stands in for a video element | getFlowConfiguration, in the background | No |
| Video files | The system player, as the screen renders | Not 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.
- In the Flow & Paywall Builder, set a custom media ID on the image or video. The file you upload there stays as the fallback.
- Add the file to your app’s
res/raworassetsfolder. - When you create the flow view with
getFlowView, pass the bundled file for that ID incustomAssets:
// "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
fetchPolicytoAdaptyPlacementFetchPolicy.ReturnCacheDataElseLoadon every fetch except the very first. - Configure a fallback paywall for every placement in the Adapty dashboard.
- Set
loadTimeoutto 3–5 seconds and accept the fallback when the timeout fires. - Don’t gate flow display on
getProfile. CallgetFlowindependently so a slow profile doesn’t block the UI.