Skip to main content
Supported on: iOS SDK 1.0.61+ · Android SDK 1.0.33+ · Flutter SDK 1.0.3+
The AppDNA Flutter SDK is designed to work reliably in offline and poor network conditions. Config caching, event queuing, and bundled fallback configs all happen at the native layer (iOS UserDefaults + Application Support; Android SharedPreferences + SQLite) for crash-safe persistence. This page documents the behavior end-to-end; for additional platform-specific internals see the iOS Offline and Android Offline guides.

Config Resolution Priority

When the SDK needs configuration data, it follows a three-tier priority order:
The SDK uses a stale-while-revalidate pattern. Cached config is served immediately, whatever its age, while a fresh copy is fetched from the remote source in the background. If that fetch fails, the cached config stays in use.

Config TTL

The cached configuration has a time-to-live (TTL) that defaults to 3600 seconds (1 hour). You can customize this value in AppDNAOptions:
When the TTL expires, the SDK fetches a fresh config from Firestore on the next access. The stale config continues to be used until the fresh config is received.

Config Update Notifications

The native layer emits a notification whenever the SDK applies a new remote config bundle (the fetch at configure, a TTL refresh, or refresh(); on Android also the 15-minute background refresh). Use AppDNA.remoteConfig.onChanged to react in Flutter:
This mirrors iOS’s Notification.Name(AppDNA.configUpdated) and Android’s AppDNA.configUpdated SharedFlow<Unit>.

Bundled Config

To ensure the SDK has a valid configuration on first launch — even without network connectivity — you can bundle a static config file with your app. Download it from the AppDNA Console with JSON Only under Settings → SDK → Config Bundle (the bundle is regenerated automatically shortly after each publish, usually within about 20 seconds; click Regenerate only if you need it at once), or in CI as in Config Bundles in CI/CD. The native SDK reads the file, so it goes in the native locations below, not under flutter: assets: in pubspec.yaml.

iOS

Add the config file to your Xcode project bundle:
  1. Save the downloaded file as appdna-config.json.
  2. Add it to your Xcode project by dragging it into the project navigator.
  3. Ensure the file is included in your target’s Build Phases > Copy Bundle Resources.

Android

Place the config file in the Android assets directory:
The bundled config is a snapshot and will become stale over time. Always ensure the SDK can reach the remote config source for the latest configuration. The bundled config should be treated as a fallback only — download it again as part of your CI/CD pipeline before each release (the server regenerates it shortly after each publish).

Bundle Versioning

The native SDK reads bundle_version from the file. When it is greater than 0, the SDK sends it in the device context of every event, so you can see which bundle a build carries. The SDK does not compare versions: it fetches remote config at configure and when the TTL elapses, whatever the file holds. The file fills only the sections the cache does not have, except screen_index and, on iOS, brand (see Three-Tier Config Priority); a remote fetch replaces it.

Event Queue

Events tracked with AppDNA.track(...) are not sent immediately. They are queued locally on the native side and flushed in batches.

Queue Behavior

Events are flushed on the timer, when the app goes to the background or returns to the foreground, and when the queue reaches the batch size (the adaptive size below, capped by batchSize when set) — whichever comes first. You can also call AppDNA.flush() to trigger an immediate flush.

Persistence

Events are persisted to disk immediately when tracked. This ensures that:
  • Events survive app crashes and force quits.
  • Events survive app restarts and device reboots.
  • Events are delivered on the next successful flush after connectivity is restored.
On iOS, the event queue is stored in the app’s Application Support directory. On Android, events are stored in a local SQLite database under the SDK’s private namespace. Events stay queued until they are delivered or dropped: a batch rejected with a 4xx other than 408 or 429, the oldest events beyond 10,000 events or 5 MB, events older than 7 days, and every queued event when analytics consent is revoked. See When Events Are Dropped.

Adaptive Batch Sizing

The SDK automatically adjusts batch size based on network conditions:
  • Wi-Fi or wired: 100 events per batch, even when the connection is expensive or metered
  • Cellular marked expensive (iOS) or metered (Android): 20 events per batch. iOS marks every cellular connection expensive; most Android mobile plans are metered
  • Other cellular: 50 events per batch
With batchSize set, the batch size is the smaller of this size and batchSize.

Event Storage Limits

Events are stored locally with the following limits:
  • Maximum 10,000 events in queue
  • Maximum 5 MB of queued events, on both platforms measured the same way: each event’s JSON plus one byte. On iOS, sent and dropped events stay in the queue file until the SDK reclaims their space (once they pass 1 MB, or outweigh the queued ones), so the file can reach about 6 MB; on Android, the SQLite database file also holds its indexes and free pages
Oldest events are dropped when limits are exceeded — over 5 MB, the oldest 10 % at a time until the rest fit — and every dropped event is counted. An event older than 7 days is dropped unsent. On iOS, a launch before the device is first unlocked (a background launch) cannot read the queue: the SDK leaves it as it is and reads it as soon as it can.

Retry Policy

When a flush attempt fails, the SDK retries with exponential backoff: After 3 failed attempts, the request is abandoned and events remain in the queue for the next flush cycle.

Error Handling

Events that receive a 4xx response other than 408 or 429 are discarded permanently. This prevents malformed events from blocking the queue. Check your event schemas if you see 4xx errors in the SDK logs.

Circuit Breaker

After 5 consecutive upload failures, event uploads are paused: the timer, the batch size and backgrounding do not start an upload (backgrounding schedules no OS background upload, and one already scheduled uploads nothing while paused). Returning to the foreground or calling AppDNA.flush() resets the failure count and resumes uploads. A single failure does not pause uploads, except a 401 or 403 (an invalid or revoked API key): every later batch would be rejected the same way, so it pauses uploads at once, in the in-app queue and the OS background upload alike. Every other failed flush counts toward the consecutive-failure total — whether a 4xx batch was dropped or a 5xx/network error left the batch queued for retry — and only the 5th failure in a row trips the circuit breaker. An OS background upload ends its run after a rejected batch; its next run sends the events behind it.

Network Timeouts

When analytics consent is set to false:
Events are silently dropped and are not added to the queue. No data is stored or transmitted. When consent is later granted, only events tracked after that point are queued and sent.
Consent state is persisted on the native side (UserDefaults on iOS, SharedPreferences on Android). If a user revokes consent and restarts the app, the SDK remembers the revoked state and continues to drop events until consent is explicitly granted again.

Secure Storage

The following data is stored securely on each platform: On iOS, Keychain storage ensures these values persist across app reinstalls and are protected by the device’s hardware encryption. On Android, EncryptedSharedPreferences uses AES-256-GCM via the Android Keystore, so values are protected by hardware encryption on supported devices.

EventChannel Streams

The SDK uses Flutter EventChannels to stream real-time data from the native layer. These streams work across the full app lifecycle, including when the app transitions between foreground and background states:
EventChannel streams automatically reconnect when the app returns from the background. You do not need to re-subscribe to streams after lifecycle transitions.

Offline Behavior Summary

Best Practices

  1. Always bundle a config file for first-launch reliability.
  2. Keep configTTL reasonable — 1 hour (default) balances freshness with network efficiency.
  3. Call flush() before background — While the SDK handles this automatically, explicitly flushing before the app enters the background provides an extra safety net.
  4. Do not assume network availability — Design your app UI to work with cached config values. onReady() fires once the bootstrap has succeeded or failed, without waiting for the remote config: the fetch starts when the bootstrap succeeds (after a failure, when a retry succeeds) — use remoteConfig.onChanged to pick up the fetched values.