Skip to main content

Initialization

Before using any AFCore feature, the SDK must be initialized with an AFCoreConfig. This page covers configuration, platform-specific initialization, readiness observation, and reconfiguration.

Building AFCoreConfig

AFCoreConfig is built using the builder pattern. It requires three mandatory fields and two optional logging fields:

FieldRequiredDescription
baseUrlYesThe API base URL (must be HTTPS)
clientIdYesOAuth client identifier
clientSecretYesOAuth client secret
enableLoggingNoEnable/disable SDK logging (default: false)
logLevelNoLog verbosity level (default: OFF)
logWriterNoCustom log writer for routing SDK logs to your logging framework
val config = AFCoreConfig.builder()
.baseUrl("https://api.example.com")
.clientId("YOUR_CLIENT_ID")
.clientSecret("YOUR_CLIENT_SECRET")
.enableLogging(BuildConfig.DEBUG)
.logLevel(AFLogLevel.DEBUG)
.build()

Log Levels

The AFLogLevel enum controls which log messages are emitted:

LevelDescription
VERBOSEAll messages, including detailed internal state
DEBUGDebug messages and above
INFOInformational messages and above
WARNWarnings and errors only
ERRORErrors only
ASSERTCritical assertions only
OFFNo logging (default)

Custom Log Writer

You can route SDK logs to your own logging framework by providing a custom AFLogWriter:

val config = AFCoreConfig.builder()
.baseUrl("https://api.example.com")
.clientId("YOUR_CLIENT_ID")
.clientSecret("YOUR_CLIENT_SECRET")
.enableLogging(true)
.logLevel(AFLogLevel.DEBUG)
.logWriter(object : AFLogWriter {
override fun log(level: AFLogLevel, tag: String, message: String) {
Timber.tag(tag).d(message) // Route to Timber
}
})
.build()

Calling initialize()

AFCore.initialize(config) must be the first SDK method called. It performs the following steps:

  1. Configures the logging pipeline.
  2. Creates or opens encrypted storage (EncryptedSharedPreferences on Android, Keychain on iOS/watchOS).
  3. Persists client credentials in encrypted storage.
  4. Builds the Ktor HttpClient with the platform engine and plugin pipeline.
  5. Persists the config in AFBootConfigStore for boot-time recovery (Android/iOS only).
  6. If a previous session exists, launches background auto-restore (Android/iOS only).

Return Value

initialize() returns a Boolean:

ReturnMeaning
trueInitialization succeeded
falseInitialization failed (error is logged)

If initialization fails, the SDK remains uninitialized. Any subsequent calls to feature accessors will throw IllegalStateException.

Platform-Specific Setup

Android

Call AFCore.initialize() in your Application.onCreate(). This runs before any Activity, Service, or BroadcastReceiver, ensuring the SDK is ready for all components.

class MyApplication : Application() {
override fun onCreate() {
super.onCreate()

val config = AFCoreConfig.builder()
.baseUrl("https://api.example.com")
.clientId("YOUR_CLIENT_ID")
.clientSecret("YOUR_CLIENT_SECRET")
.enableLogging(BuildConfig.DEBUG)
.logLevel(if (BuildConfig.DEBUG) AFLogLevel.DEBUG else AFLogLevel.OFF)
.build()

val success = AFCore.initialize(config)
if (!success) {
Log.e("MyApp", "AFCore initialization failed")
}
}
}
AFContextProvider

AFCore uses Android's App Startup library (AFContextProvider) to obtain the Application context automatically. You do not need to pass a Context to initialize().

iOS (AppDelegate)

Call AFCore.shared.initialize() in application(_:didFinishLaunchingWithOptions:):

import UIKit
import AFCore

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

let config = AFCoreConfig.builder()
.baseUrl("https://api.example.com")
.clientId("YOUR_CLIENT_ID")
.clientSecret("YOUR_CLIENT_SECRET")
.enableLogging(true)
.logLevel(.debug)
.build()

let success = AFCore.shared.initialize(config: config)
if !success {
print("AFCore initialization failed")
}

return true
}
}

iOS (SwiftUI App)

For SwiftUI lifecycle apps, initialize in the App struct's init():

import SwiftUI
import AFCore

@main
struct MyApp: App {
init() {
let config = AFCoreConfig.builder()
.baseUrl("https://api.example.com")
.clientId("YOUR_CLIENT_ID")
.clientSecret("YOUR_CLIENT_SECRET")
.enableLogging(true)
.logLevel(.debug)
.build()

let _ = AFCore.shared.initialize(config: config)
}

var body: some Scene {
WindowGroup {
ContentView()
}
}
}

watchOS

watchOS initialization is synchronous -- there is no auto-restore, so isReady is set to true immediately after initialize() completes:

import WatchKit
import AFCore

class ExtensionDelegate: NSObject, WKApplicationDelegate {
func applicationDidFinishLaunching() {
let config = AFCoreConfig.builder()
.baseUrl("https://api.example.com")
.clientId("YOUR_CLIENT_ID")
.clientSecret("YOUR_CLIENT_SECRET")
.build()

let _ = AFCore.shared.initialize(config: config)
// isReady is already true here -- no auto-restore on watchOS
}
}

Observing Readiness

On Android and iOS, initialize() may launch a background coroutine to restore geofences and SmartWalking auto-sync from a previous session. During this time, isReady remains false. It becomes true once all auto-restore work has completed (or immediately if there is no session to restore).

Best practice: Defer navigation or data-fetching UI until isReady is true to avoid races with auto-restore coroutines.

// In an Activity or Fragment
lifecycleScope.launch {
AFCore.isReady.first { it }
// SDK is fully initialized -- safe to proceed
navigateToHome()
}

What Auto-Restore Does

When initialize() detects an existing session (non-blank access token), it:

  1. Fetches entitlements -- Calls programs().getAvailablePrograms() to validate the session and check feature entitlements. This also triggers a token refresh if the token is near expiry.
  2. Restores geofencing -- If geofencing was previously active (preferences.geofencingStarted == true), restores geofence monitoring using the persisted geofence state.
  3. Restores SmartWalking -- If auto-sync was enabled (preferences.smartWalkingAutoSyncEnabled == true) and the member still has the SmartWalking entitlement, re-enables auto-sync and triggers an immediate step sync.

If any of these steps fail, the failure is logged but does not prevent isReady from becoming true. Auto-restore failures are non-fatal.

Idempotency

initialize() is idempotent when called with the same configuration:

ScenarioBehavior
Same config, already initializedNo-op, returns true
Different config, already initializedLogs a warning, returns without re-initializing
Not yet initializedPerforms full initialization

To change the configuration after initialization, use reconfigure() instead of calling initialize() again.

Reconfiguration

AFCore.reconfigure(config) tears down the current SDK state and re-initializes with a new configuration. This is useful for switching environments at runtime (e.g., staging to production) or swapping client credentials.

// Switch to staging environment
val stagingConfig = AFCoreConfig.builder()
.baseUrl("https://staging.example.com")
.clientId("STAGING_CLIENT_ID")
.clientSecret("STAGING_CLIENT_SECRET")
.enableLogging(true)
.logLevel(AFLogLevel.VERBOSE)
.build()

val success = AFCore.reconfigure(stagingConfig)

What reconfigure() Does

  1. Resets all state: Clears cached module instances, emits SessionState.LOGGED_OUT, sets isReady to false.
  2. Re-initializes: Calls initialize() with the new config.
caution

reconfigure() clears the current session. The user will need to re-authenticate after reconfiguration. Do not call this during normal operation -- it is intended for environment switching or credential rotation.

Thread Safety

PlatformMechanism
Android@Synchronized on initialize(), reconfigure(), and reset(). Prevents concurrent re-initialization from multiple threads (e.g., main thread + WorkManager callback).
iOS@Volatile fields ensure cross-thread visibility. Kotlin/Native's concurrency model provides additional safety.
watchOSSame as iOS, but simpler since there is no background work.

Error Handling

If initialize() returns false, check the SDK logs for the specific error. Common failure causes:

CausePlatformResolution
Keychain access deniediOSEnsure the app has the Keychain Sharing entitlement if needed
Android Keystore unavailableAndroidUsually a device-level issue; the SDK attempts to recreate corrupted preferences
Invalid base URLAllEnsure the URL starts with https:// and contains a valid host
Empty client credentialsAllEnsure clientId and clientSecret are non-empty