Supported on: iOS SDK
1.0.61+ · Android SDK 1.0.33+ · Flutter SDK 1.0.3+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 inAppDNAOptions:
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 atconfigure, a TTL refresh, or refresh(); on Android also the 15-minute background refresh). Use AppDNA.remoteConfig.onChanged to react in Flutter:
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 underflutter: assets: in pubspec.yaml.
iOS
Add the config file to your Xcode project bundle:- Save the downloaded file as
appdna-config.json. - Add it to your Xcode project by dragging it into the project navigator.
- Ensure the file is included in your target’s Build Phases > Copy Bundle Resources.
Android
Place the config file in the Android assets directory:Bundle Versioning
The native SDK readsbundle_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 withAppDNA.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
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
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
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 callingAppDNA.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
Consent and Offline Behavior
When analytics consent is set tofalse:
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 FlutterEventChannels 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
- Always bundle a config file for first-launch reliability.
- Keep
configTTLreasonable — 1 hour (default) balances freshness with network efficiency. - Call
flush()before background — While the SDK handles this automatically, explicitly flushing before the app enters the background provides an extra safety net. - 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) — useremoteConfig.onChangedto pick up the fetched values.

