Flutter SDK / Caching, offline & errors
Caching, offline & errors
The SDK is built for the realities of mobile: flows render instantly from cache, survive dead networks, and fail with typed, actionable errors when there's truly nothing to show.
Fetch behavior
| Situation | Type | Description |
|---|---|---|
| warm cache | source: "cache" | Renders instantly from disk; a background conditional request (ETag) revalidates the cache for the next launch. |
| cold start | source: "network" | No cached copy yet — the SDK awaits the network (8s timeout), then caches the flow. |
| updated in background | source: "cacheRevalidated" | The cached copy was served and the background revalidation found a newer version for next time. |
| offline / server error | cache fallback | Any transport error or non-2xx silently falls back to the cached copy. Users onboard even in airplane mode. |
Pass forceRefresh: true to skip the cache — useful right after you deploy while testing. See Versions & caching for the server side of this story.
Errors
An error only surfaces when there is no cached copy to fall back on. The engine renders its own error state, inside your widget tree, with a retry — you do not wire anything up for this. It classifies the failure as one of:
| Kind | Type | Description |
|---|---|---|
| notFound | 404 | No flow with that key — check the key, and that the flow is deployed. |
| unauthorized / forbidden | 401 / 403 | The API key is missing, revoked, or belongs to a different app. |
| network | no response | Timeout or no connectivity, with an empty cache. |
| invalidPayload | bad body | The response wasn't valid flow JSON (e.g. a proxy error page). |
| server | other non-2xx | An unexpected server-side failure. |
What a Flutter host can observe
One callback, at the end. There is no session object on the Dart side to subscribe to, because there is no session on the Dart side: the transition graph, the answers and the event queue live in the engine, in one implementation.
UpliftFunnelFlow(
'welcome',
forceRefresh: false,
onCompleted: (result) {
result.source; // 'cache' | 'network' | 'cacheRevalidated'
result.endReason; // 'completed' | 'abandoned' | 'skipped'
result.variables; // every answer collected over the session
},
)This is the named cost of the 0.8.0 change, and it is worth stating plainly rather than burying: a Flutter host can no longer read mid-session state, observe an event stream, drive navigation, or swap the loading and error views. What it bought is that a flow renders identically in a Flutter app and a native one — the two used to be separate implementations of the same document, and they drifted quietly enough that a type ramp was wrong in four of six roles without a test failing.
Everything the flow needs your app to do — take a payment, restore one, sign a user in, ask for a permission, pick a photo, open a link — is still yours, through the handlers in Native handoffs. Those run mid-flow. It is only observation and custom chrome that moved.
A native iOS app can still substitute its own loading and error views through UpliftFunnelFlowView(loadingView:errorView:). If you need that in Flutter, tell us — it is a bridge addition, not a limitation of the engine.