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:
| Field | Required | Description |
|---|---|---|
baseUrl | Yes | The API base URL (must be HTTPS) |
clientId | Yes | OAuth client identifier |
clientSecret | Yes | OAuth client secret |
enableLogging | No | Enable/disable SDK logging (default: false) |
logLevel | No | Log verbosity level (default: OFF) |
logWriter | No | Custom log writer for routing SDK logs to your logging framework |
- Android (Kotlin)
- iOS (Swift)
val config = AFCoreConfig.builder()
.baseUrl("https://api.example.com")
.clientId("YOUR_CLIENT_ID")
.clientSecret("YOUR_CLIENT_SECRET")
.enableLogging(BuildConfig.DEBUG)
.logLevel(AFLogLevel.DEBUG)
.build()
let config = AFCoreConfig.builder()
.baseUrl("https://api.example.com")
.clientId("YOUR_CLIENT_ID")
.clientSecret("YOUR_CLIENT_SECRET")
.enableLogging(true)
.logLevel(.debug)
.build()
Log Levels
The AFLogLevel enum controls which log messages are emitted:
| Level | Description |
|---|---|
VERBOSE | All messages, including detailed internal state |
DEBUG | Debug messages and above |
INFO | Informational messages and above |
WARN | Warnings and errors only |
ERROR | Errors only |
ASSERT | Critical assertions only |
OFF | No logging (default) |
Custom Log Writer
You can route SDK logs to your own logging framework by providing a custom AFLogWriter:
- Android (Kotlin)
- iOS (Swift)
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()
class MyLogWriter: AFLogWriter {
func log(level: AFLogLevel, tag: String, message: String) {
os_log("%{public}@: %{public}@", tag, message)
}
}
let config = AFCoreConfig.builder()
.baseUrl("https://api.example.com")
.clientId("YOUR_CLIENT_ID")
.clientSecret("YOUR_CLIENT_SECRET")
.enableLogging(true)
.logLevel(.debug)
.logWriter(MyLogWriter())
.build()
Calling initialize()
AFCore.initialize(config) must be the first SDK method called. It performs the following steps:
- Configures the logging pipeline.
- Creates or opens encrypted storage (EncryptedSharedPreferences on Android, Keychain on iOS/watchOS).
- Persists client credentials in encrypted storage.
- Builds the Ktor
HttpClientwith the platform engine and plugin pipeline. - Persists the config in
AFBootConfigStorefor boot-time recovery (Android/iOS only). - If a previous session exists, launches background auto-restore (Android/iOS only).
Return Value
initialize() returns a Boolean:
| Return | Meaning |
|---|---|
true | Initialization succeeded |
false | Initialization 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")
}
}
}
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.
- Android (Kotlin)
- iOS (Swift)
// In an Activity or Fragment
lifecycleScope.launch {
AFCore.isReady.first { it }
// SDK is fully initialized -- safe to proceed
navigateToHome()
}
// Using async/await
Task {
for await ready in AFCore.shared.isReady where ready {
navigateToHome()
break
}
}
What Auto-Restore Does
When initialize() detects an existing session (non-blank access token), it:
- 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. - Restores geofencing -- If geofencing was previously active (
preferences.geofencingStarted == true), restores geofence monitoring using the persisted geofence state. - 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:
| Scenario | Behavior |
|---|---|
| Same config, already initialized | No-op, returns true |
| Different config, already initialized | Logs a warning, returns without re-initializing |
| Not yet initialized | Performs 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.
- Android (Kotlin)
- iOS (Swift)
// 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)
let stagingConfig = AFCoreConfig.builder()
.baseUrl("https://staging.example.com")
.clientId("STAGING_CLIENT_ID")
.clientSecret("STAGING_CLIENT_SECRET")
.enableLogging(true)
.logLevel(.verbose)
.build()
let success = AFCore.shared.reconfigure(config: stagingConfig)
What reconfigure() Does
- Resets all state: Clears cached module instances, emits
SessionState.LOGGED_OUT, setsisReadytofalse. - Re-initializes: Calls
initialize()with the new config.
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
| Platform | Mechanism |
|---|---|
| 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. |
| watchOS | Same 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:
| Cause | Platform | Resolution |
|---|---|---|
| Keychain access denied | iOS | Ensure the app has the Keychain Sharing entitlement if needed |
| Android Keystore unavailable | Android | Usually a device-level issue; the SDK attempts to recreate corrupted preferences |
| Invalid base URL | All | Ensure the URL starts with https:// and contains a valid host |
| Empty client credentials | All | Ensure clientId and clientSecret are non-empty |