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:
| Priority | Source | Description |
|---|
| 1 (highest) | Remote | Firestore real-time listener for live configuration |
| 2 | Cached | UserDefaults (iOS) / SharedPreferences (Android) with TTL |
| 3 (lowest) | Bundled | Static appdna-config.json shipped with the app binary |
The SDK uses a stale-while-revalidate pattern. Cached config is served immediately while a fresh copy is fetched from the remote source in the background.
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:
await AppDNA.configure(
apiKey: "adn_live_xxx",
env: AppDNAEnvironment.production,
options: AppDNAOptions(
configTTL: 1800, // 30 minutes
),
);
| Parameter | Type | Default | Description |
|---|
configTTL | int? | 3600 | Seconds before cached config is considered stale |
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 (initial load, TTL refresh, or live Firestore push). Use AppDNA.remoteConfig.onChanged to react in Flutter:
AppDNA.remoteConfig.onChanged(() {
// Config updated — re-read remote values and refresh UI
});
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. The file should be downloaded from the AppDNA Console under SDK > Config Bundle.
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:
android/app/src/main/assets/appdna-config.json
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 — regenerate and replace it as part of your CI/CD pipeline before each release.
Bundle Versioning
The bundle version is managed by the native layer. When the SDK resolves config, it compares the cached config version with the remote version to determine whether an update is needed. The bundled config is treated as the lowest-priority source and is always superseded by any cached or remote config.
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
| Setting | Default | Description |
|---|
flushInterval | 30s | Time interval between automatic flush cycles |
batchSize | 20 | Number of events to batch before auto-flushing |
Events are flushed when either condition is met (whichever occurs first). You can also call AppDNA.flush() to trigger an immediate flush.
await AppDNA.configure(
apiKey: "adn_live_xxx",
env: AppDNAEnvironment.production,
options: AppDNAOptions(
flushInterval: 30, // Seconds between automatic flushes
batchSize: 20, // Events per batch
),
);
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 remain queued until they are successfully delivered to the AppDNA backend or the user revokes analytics consent.
Adaptive Batch Sizing
The SDK automatically adjusts batch size based on network conditions:
- Wi-Fi: 100 events per batch
- Cellular: 50 events per batch
- Expensive cellular (roaming / data saver): 20 events per batch
Event Storage Limits
Events are stored locally with the following limits:
- Maximum 10,000 events in queue
- Maximum 5MB disk usage
Oldest events are dropped when limits are exceeded.
Retry Policy
When a flush attempt fails, the SDK retries with exponential backoff:
| Attempt | Delay | Description |
|---|
| 1 | 1s | First retry after initial failure |
| 2 | 2s | Second retry |
| 3 | 4s | Third and final retry |
After 3 failed attempts, the request is abandoned and events remain in the queue for the next flush cycle.
Error Handling
| Status Code | Behavior |
|---|
| 2xx | Success. Events removed from queue. |
| 4xx | Client error. Events discarded immediately (no retry). |
| 5xx | Server error. Events kept in queue and retried. |
| Network error | Events kept in queue and retried on next flush cycle. |
Events that receive a 4xx response 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 until the next app session. Server errors (4xx) immediately pause uploads for the current session.
Network Timeouts
| Operation | Timeout |
|---|
| API requests | 30s |
| Resource downloads | 60s |
Consent and Offline Behavior
When analytics consent is set to false:
await AppDNA.setConsent(analytics: 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:
| Data | iOS Storage | Android Storage | Description |
|---|
| Push tokens | Keychain | EncryptedSharedPreferences | APNs / FCM device token |
| User IDs | Keychain | EncryptedSharedPreferences | Identified user ID |
| Anonymous IDs | Keychain | EncryptedSharedPreferences | Auto-generated anonymous identifier |
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 | Purpose |
|---|
com.appdna.sdk/web_entitlement | Web entitlement change stream |
com.appdna.sdk/events/push | Push notification delegate events |
com.appdna.sdk/events/paywall | Paywall delegate events |
com.appdna.sdk/events/onboarding | Onboarding delegate events |
com.appdna.sdk/events/in_app_message | In-app message delegate events |
com.appdna.sdk/events/survey | Survey delegate events |
com.appdna.sdk/events/deep_link | Deep link delegate events |
com.appdna.sdk/events/screen | Server-driven screen delegate events |
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
| Concern | Behavior |
|---|
| Config on first launch | Bundled appdna-config.json used as fallback |
| Config caching | Native storage with configurable TTL (default 3600s / 1 hour) |
| Config freshness | Stale-while-revalidate; Firestore real-time listener |
| Event persistence | Written to disk immediately, survives crashes and restarts |
| Event flush | Automatic on timer (30s) or batch threshold (20 events) |
| Retry | Up to 3 retries with exponential backoff (1s, 2s, 4s) |
| 4xx errors | Events discarded, no retry |
| 5xx / network errors | Events retained, retried on next flush |
| Circuit breaker | After 5 consecutive failures, uploads paused until next session |
| Consent revoked | Events dropped silently, not queued |
| Feature flags | Uses cached / bundled values until remote config is refreshed |
| Experiments | Variant assignments are cached; new assignments wait for network |
| Push registration | Token registration is retried when connectivity is restored |
| Purchases | Handled by the native store SDK (StoreKit / Play Billing) |
| Sensitive data | Keychain (iOS) / EncryptedSharedPreferences (Android) |
Best Practices
- Always bundle a config file for first-launch reliability.
- Keep
configTTL reasonable — 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. Use
onReady() to know when remote config has been loaded.
// Flush events before background
void onAppLifecycleStateChanged(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
AppDNA.flush();
}
}