Skip to main content

Frequently Asked Questions

Which platforms does AFCore support?

AFCore supports three platforms:

PlatformMinimum VersionNotes
AndroidAPI 24 (Android 7.0)Full feature set
iOS16.0Full feature set
watchOS9.0Limited -- no geofencing, no barcode, no proximity, no background sync

AFCore is built with Kotlin Multiplatform (KMP), sharing business logic across all platforms while using native APIs for storage, networking, and background work.

Is AFCore thread-safe?

Yes. All public APIs are safe to call from any thread or coroutine context. Internally, the SDK uses multiple concurrency mechanisms:

MechanismUsed For
@VolatileLazy singleton references for feature modules
@SynchronizedInitialization on Android (prevents concurrent initialize() calls)
MutexCache read/write, token refresh operations
CompletableDeferredRequest coalescing for concurrent identical GET requests
SupervisorJobInternal AFCoreScope isolates background failures

You do not need to add your own synchronization around SDK calls.

Do I need to manage tokens?

No. Token management is fully automatic:

  • The DefaultTokenManager monitors token expiration with a 5-minute threshold (refreshes 5 minutes before expiry)
  • On refresh failure, it retries with exponential backoff (1 second, 2 seconds, 4 seconds ... up to 30 seconds)
  • All token operations are mutex-protected to prevent concurrent refresh races
  • If the token cannot be refreshed, the SDK emits SESSION_EXPIRED on the sessionState flow

Your only responsibility is to observe sessionState and prompt re-login when the session expires:

viewModelScope.launch {
AFCore.authentication().sessionState.collect { state ->
if (state == SessionState.SESSION_EXPIRED) {
navigateToLogin()
}
}
}

Can I use AFCore from Java?

Yes, but Kotlin is recommended. Since AFCore's API uses Kotlin suspend functions, you need coroutine wrappers to call them from Java:

// Java: Use kotlinx.coroutines to call suspend functions
BuildersKt.launch(
GlobalScope.INSTANCE,
Dispatchers.getMain(),
CoroutineStart.DEFAULT,
(scope, continuation) -> {
AFCore.INSTANCE.profile().get(continuation);
return Unit.INSTANCE;
}
);

Alternatively, create Kotlin wrapper functions that expose callbacks:

// Kotlin wrapper for Java consumers
object AFCoreJavaHelper {
@JvmStatic
fun getProfile(callback: (AFProfile?, Exception?) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
try {
val profile = AFCore.profile().get()
callback(profile, null)
} catch (e: Exception) {
callback(null, e)
}
}
}
}
// Java: Call the wrapper
AFCoreJavaHelper.getProfile((profile, error) -> {
if (error != null) {
showError(error.getMessage());
} else {
updateUI(profile);
}
});

Can I use AFCore from Objective-C?

Yes. Kotlin Multiplatform generates Objective-C-compatible headers for iOS. Suspend functions are automatically converted to completion handler-based APIs:

// Objective-C: Suspend functions become completion handler calls
[[AFCore authentication] loginWithUsername:@"user@example.com"
password:@"password"
completionHandler:^(AFLoginResult *result, NSError *error) {
if (error) {
NSLog(@"Login failed: %@", error.localizedDescription);
} else {
NSLog(@"Login succeeded");
}
}];

[[AFCore profile] getWithCompletionHandler:^(AFProfile *profile, NSError *error) {
if (error) {
NSLog(@"Failed to fetch profile: %@", error.localizedDescription);
} else {
NSLog(@"Name: %@", profile.name);
}
}];

How do I handle session expiration?

Observe the sessionState flow. When the state transitions to SESSION_EXPIRED, prompt the user to log in again.

class MainViewModel : ViewModel() {
init {
viewModelScope.launch {
AFCore.sessionState.collect { state ->
when (state) {
SessionState.LOGGED_IN -> { /* User is logged in */ }
SessionState.SESSION_EXPIRED -> {
// Token refresh failed after retries
// Prompt user to re-authenticate
_navigateToLogin.emit(Unit)
}
SessionState.LOGGED_OUT -> {
// User has not logged in yet or has logged out
_navigateToLogin.emit(Unit)
}
}
}
}
}
}

Does the SDK cache responses?

Yes. AFCore includes an internal AFCoreCachePlugin with the following behavior:

PropertyValue
Cached methodsGET only
TTL15 minutes per entry
Max entries100 (LRU eviction)
Request coalescingConcurrent identical GETs are merged
MutationsPOST, PUT, DELETE always skip the cache
Manual bypassUse skipCache parameter on GET requests
PersistenceIn-memory only (cleared on process kill)

You do not need to implement your own caching layer.

How does geofencing survive app kill?

Geofence survival works at two levels:

  1. OS level: Both Android and iOS maintain geofence registrations in the OS independently of your app process. When a geofence triggers, the OS wakes your app (or relaunches it if killed).

  2. SDK level: AFCore persists its internal geofence state in AFBootConfigStore. When the app relaunches, AFCore.initialize() reads this state and restores the SDK's bookkeeping to match the OS-level registrations.

This two-layer approach means geofences continue to work even after:

  • The user swipes the app away from the task switcher
  • The OS kills the app for memory pressure
  • The device reboots (Android with RECEIVE_BOOT_COMPLETED)

No action is required from you beyond calling initialize() in your app entry point.

What is the difference between syncSteps() and submitActivities()?

These two methods serve different use cases:

MethodInputUse Case
syncSteps()None (reads from HealthKit / Health Connect automatically)Standard step sync -- the SDK handles everything
submitActivities(healthData)Pre-fetched HealthData objectAdvanced use -- you provide your own health data

For most apps, use syncSteps() or enableAutoSync(). Use submitActivities() only if you are reading health data yourself and need to submit it in a custom format.

// Standard approach: Let the SDK read and submit steps
AFCore.smartWalking().syncSteps()

// Or enable auto-sync and let the SDK handle scheduling
AFCore.smartWalking().enableAutoSync()

// Advanced: Submit your own health data
val healthData = HealthData(
steps = listOf(StepRecord(date = "2026-02-22", count = 8500))
)
AFCore.smartWalking().submitActivities(healthData)

Can I customize the cache TTL?

No. The 15-minute TTL is configured internally in AFCoreCachePlugin and is not currently exposed in the public API. This value was chosen to balance freshness with network efficiency for the typical usage patterns of fitness and wellness data.

If you need data fresher than 15 minutes for a specific call, use the skipCache parameter:

// Force a fresh response, bypassing the cache
val profile = AFCore.profile().get(skipCache = true)

What data is encrypted?

AFCore uses platform-native encryption for sensitive data:

DataStorageEncrypted
Auth tokens (access, refresh)EncryptedSharedPreferences (Android) / Keychain (iOS)Yes
User profile dataEncryptedSharedPreferences / KeychainYes
Feature state (AFPreferences)EncryptedSharedPreferences / KeychainYes
Boot config (baseUrl, clientId)Standard SharedPreferences / UserDefaultsNo
HTTP cacheIn-memory onlyN/A (not persisted)

Boot configuration is stored in plain preferences intentionally. It must be readable before the user unlocks the device so the SDK can restore geofences and background tasks on boot.

note

Auth tokens and user data are always encrypted at rest. The unencrypted boot config contains only non-sensitive configuration values (base URL, client ID) needed for early initialization.

How do I enable logging?

Configure logging in the AFCoreConfig builder. You can set the log level and optionally provide a custom log writer.

AFCore.initialize(
AFCoreConfig.Builder(context = this)
.baseUrl("https://api.example.com")
.clientId("your-client-id")
.clientSecret("your-client-secret")
.enableLogging(true)
.logLevel(AFLogLevel.DEBUG) // VERBOSE, DEBUG, INFO, WARN, ERROR
.build()
)

For custom log output (e.g., writing to a file or crash reporting service):

AFCore.initialize(
AFCoreConfig.Builder(context = this)
.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) {
// Send to Crashlytics, file, or custom logger
Crashlytics.log("[$level] $tag: $message")
}
})
.build()
)

Log Levels

LevelOutput
VERBOSEEverything, including raw HTTP bodies
DEBUGDetailed internal state, cache hits/misses, token refresh events
INFOHigh-level lifecycle events (init, login, sync)
WARNRecoverable issues (retry attempts, fallback behavior)
ERRORFailures that affect functionality
caution

Do not ship production builds with VERBOSE or DEBUG logging enabled. These levels include sensitive information such as HTTP request/response bodies and token details. Use WARN or ERROR for production.