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:
PrioritySourceDescription
1 (highest)RemoteFirestore real-time listener for live configuration
2CachedUserDefaults (iOS) / SharedPreferences (Android) with TTL
3 (lowest)BundledStatic 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
  ),
);
ParameterTypeDefaultDescription
configTTLint?3600Seconds 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:
  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:
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

SettingDefaultDescription
flushInterval30sTime interval between automatic flush cycles
batchSize20Number 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:
AttemptDelayDescription
11sFirst retry after initial failure
22sSecond retry
34sThird 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 CodeBehavior
2xxSuccess. Events removed from queue.
4xxClient error. Events discarded immediately (no retry).
5xxServer error. Events kept in queue and retried.
Network errorEvents 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

OperationTimeout
API requests30s
Resource downloads60s
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:
DataiOS StorageAndroid StorageDescription
Push tokensKeychainEncryptedSharedPreferencesAPNs / FCM device token
User IDsKeychainEncryptedSharedPreferencesIdentified user ID
Anonymous IDsKeychainEncryptedSharedPreferencesAuto-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:
EventChannelPurpose
com.appdna.sdk/web_entitlementWeb entitlement change stream
com.appdna.sdk/events/pushPush notification delegate events
com.appdna.sdk/events/paywallPaywall delegate events
com.appdna.sdk/events/onboardingOnboarding delegate events
com.appdna.sdk/events/in_app_messageIn-app message delegate events
com.appdna.sdk/events/surveySurvey delegate events
com.appdna.sdk/events/deep_linkDeep link delegate events
com.appdna.sdk/events/screenServer-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

ConcernBehavior
Config on first launchBundled appdna-config.json used as fallback
Config cachingNative storage with configurable TTL (default 3600s / 1 hour)
Config freshnessStale-while-revalidate; Firestore real-time listener
Event persistenceWritten to disk immediately, survives crashes and restarts
Event flushAutomatic on timer (30s) or batch threshold (20 events)
RetryUp to 3 retries with exponential backoff (1s, 2s, 4s)
4xx errorsEvents discarded, no retry
5xx / network errorsEvents retained, retried on next flush
Circuit breakerAfter 5 consecutive failures, uploads paused until next session
Consent revokedEvents dropped silently, not queued
Feature flagsUses cached / bundled values until remote config is refreshed
ExperimentsVariant assignments are cached; new assignments wait for network
Push registrationToken registration is retried when connectivity is restored
PurchasesHandled by the native store SDK (StoreKit / Play Billing)
Sensitive dataKeychain (iOS) / EncryptedSharedPreferences (Android)

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. Use onReady() to know when remote config has been loaded.
// Flush events before background
void onAppLifecycleStateChanged(AppLifecycleState state) {
  if (state == AppLifecycleState.paused) {
    AppDNA.flush();
  }
}