Common Issues
This page covers the most frequently encountered issues when integrating the AFCore SDK, along with their symptoms, causes, and solutions.
1. "AFCore has not been initialized"
Symptoms: App crashes with IllegalStateException or logs an error message stating that AFCore has not been initialized.
Cause: A feature module (e.g., AFCore.activities(), AFCore.profile()) was accessed before AFCore.initialize() was called.
Solution: Ensure initialize() is called in your application entry point before any other AFCore usage.
- Android (Kotlin)
- iOS (Swift)
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Initialize AFCore FIRST — before any Activity or Service can use it
AFCore.initialize(
AFCoreConfig.Builder(context = this)
.baseUrl("https://api.example.com")
.clientId("your-client-id")
.clientSecret("your-client-secret")
.build()
)
}
}
@main
struct MyApp: App {
init() {
// Initialize AFCore FIRST
AFCore.initialize(
config: AFCoreConfig.Builder()
.baseUrl(url: "https://api.example.com")
.clientId(id: "your-client-id")
.clientSecret(secret: "your-client-secret")
.build()
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Common pitfalls:
- Calling
AFCore.profile()in aContentProviderthat runs beforeApplication.onCreate()(Android) - Accessing AFCore from a SwiftUI view
init()that runs before the app'sinit()(iOS) - Using AFCore in a background service that starts before the application class initializes
2. "AFCore is already initialized with a different configuration"
Symptoms: An exception is thrown on the second call to AFCore.initialize().
Cause: initialize() was called twice with different AFCoreConfig values. AFCore is a singleton and cannot be re-initialized with a different configuration.
Solution: Call initialize() only once. If you need to change configuration at runtime, use reconfigure().
- Android (Kotlin)
- iOS (Swift)
// If you need to switch environments:
val newConfig = AFCoreConfig.Builder(context = applicationContext)
.baseUrl("https://api-v2.example.com")
.clientId("new-client-id")
.clientSecret("new-client-secret")
.build()
AFCore.reconfigure(newConfig) // Safe — atomically swaps configuration
// If you need to switch environments:
let newConfig = AFCoreConfig.Builder()
.baseUrl(url: "https://api-v2.example.com")
.clientId(id: "new-client-id")
.clientSecret(secret: "new-client-secret")
.build()
AFCore.reconfigure(config: newConfig) // Safe — atomically swaps configuration
See the Initialization guide for details on reconfigure().
3. Session Expires Unexpectedly
Symptoms: The user is unexpectedly logged out. The sessionState flow emits SESSION_EXPIRED.
Cause: The SDK's automatic token refresh failed. This can happen due to:
- No network connectivity during the refresh window
- Incorrect
clientIdorclientSecret - Server-side token revocation
- Exponential backoff exhausted (1s, 2s, 4s, 8s, 16s, 30s)
Diagnosis:
- Enable debug logging to see token refresh attempts:
AFCoreConfig.Builder(context = this)
.enableLogging(true)
.logLevel(AFLogLevel.DEBUG)
.build()
-
Look for log messages containing
TokenManagerorbackoffto see refresh attempts and their results. -
Check for
401or403HTTP status codes in the logs, which indicate credential issues.
Solution:
- Verify
clientIdandclientSecretare correct - Ensure the device has network connectivity
- Observe
sessionStateand prompt the user to re-login onSESSION_EXPIRED - Do not implement your own token refresh logic -- the SDK handles this automatically
4. Geofences Not Triggering
Symptoms: The app does not receive geofence entry/exit events. The user visits a facility but no automatic visit is recorded.
Cause: Several factors can prevent geofence triggers:
| Cause | Solution |
|---|---|
Missing ACCESS_BACKGROUND_LOCATION permission (Android) | Request the permission at runtime; check AFGeofencing.getStatus() |
| Location services disabled | Prompt the user to enable location in system settings |
| Radius too small (< 100m) | Use a radius of at least 100 meters for reliable triggers |
| GPS not available indoors | Move near windows or doors; Wi-Fi-based location may help |
| Too many geofences registered (OS limit) | Android allows 100, iOS allows 20 per app |
| Battery saver / low-power mode | OS may throttle location updates aggressively |
Diagnosis:
- Android (Kotlin)
- iOS (Swift)
lifecycleScope.launch {
val status = AFCore.geofencing().getStatus()
Log.d("Geofence", "Is monitoring: ${status.isMonitoring}")
Log.d("Geofence", "Has permission: ${status.hasPermission}")
Log.d("Geofence", "Location enabled: ${status.isLocationEnabled}")
Log.d("Geofence", "Registered count: ${status.registeredGeofences}")
}
Task {
let status = try await AFCore.geofencing().getStatus()
print("Is monitoring: \(status.isMonitoring)")
print("Has permission: \(status.hasPermission)")
print("Location enabled: \(status.isLocationEnabled)")
print("Registered count: \(status.registeredGeofences)")
}
5. SmartWalking Sync Not Working
Symptoms: Step data is not being submitted to the server. The activity dashboard shows no SmartWalking entries despite the user walking.
Cause: Common causes include:
| Cause | Solution |
|---|---|
| Health permissions not granted | Request HealthKit (iOS) or Health Connect (Android) permissions |
| Auto-sync not enabled | Call enableAutoSync() after initialization |
| No network connectivity | Sync will retry automatically when network is available |
| Battery too low (Android) | WorkManager defers work when battery is critically low |
Diagnosis:
- Android (Kotlin)
- iOS (Swift)
lifecycleScope.launch {
val diagnostics = AFCore.smartWalking().getDiagnostics()
Log.d("SW", "Auto-sync enabled: ${diagnostics.isAutoSyncEnabled}")
Log.d("SW", "Health permission: ${diagnostics.hasHealthPermission}")
Log.d("SW", "Last sync: ${diagnostics.lastSyncTime}")
Log.d("SW", "Last sync result: ${diagnostics.lastSyncResult}")
}
Task {
let diagnostics = try await AFCore.smartWalking().getDiagnostics()
print("Auto-sync enabled: \(diagnostics.isAutoSyncEnabled)")
print("Health permission: \(diagnostics.hasHealthPermission)")
print("Last sync: \(diagnostics.lastSyncTime)")
print("Last sync result: \(diagnostics.lastSyncResult)")
}
6. HealthKit Permissions Show LIKELY_DENIED
Symptoms: hasPermissions() returns LIKELY_DENIED for HealthKit data types, even though you believe the user granted permission.
Cause: iOS does not reveal the exact HealthKit authorization status for privacy reasons. When the SDK requests step data and receives no results, it reports LIKELY_DENIED. This does not necessarily mean the user explicitly denied permission -- they may have never been prompted, or they may have granted permission but have no step data for the requested period.
Solution:
- Guide the user to Settings > Health > Data Access & Devices to verify the permission is granted
- Ensure you are requesting data for a period when the user actually had step data
- Call the permission request flow again -- iOS will show the prompt if the user has not yet responded
- Note that
LIKELY_DENIEDis the SDK's best guess. There is no API on iOS to check HealthKit authorization state for read permissions definitively.
7. iOS Keychain Data Stale After Reinstall
Symptoms: After uninstalling and reinstalling the app, the SDK appears to have stale authentication tokens or user data from the previous installation.
Cause: iOS Keychain data persists across app uninstall/reinstall by default. The SDK uses a ReinstallSentinel mechanism to detect reinstalls and clear stale Keychain data, but it may not work if:
- The Keychain accessibility is not set to
kSecAttrAccessibleAfterFirstUnlock - The sentinel value was corrupted
Solution:
- Verify that your app's Keychain accessibility is set to
kSecAttrAccessibleAfterFirstUnlock - On first launch after reinstall, the SDK should automatically detect the reinstall and clear stale data
- If the issue persists, call
AFCore.authentication().logout()to clear all stored credentials
8. Android EncryptedSharedPreferences Crash
Symptoms: SecurityException or InvalidKeyException crash on Android when the SDK tries to read or write encrypted preferences.
Cause: This typically happens when:
- The device was migrated from another device (Android backup/restore)
- The device's secure hardware state changed (factory reset with data migration)
- The Android Keystore master key was corrupted
Solution: Catch the exception and clear the corrupted preferences:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
try {
AFCore.initialize(
AFCoreConfig.Builder(context = this)
.baseUrl("https://api.example.com")
.clientId("your-client-id")
.clientSecret("your-client-secret")
.build()
)
} catch (e: SecurityException) {
// Clear corrupted encrypted preferences
Log.e("AFCore", "EncryptedSharedPreferences corrupted, clearing", e)
getSharedPreferences("af_core_prefs", MODE_PRIVATE).edit().clear().apply()
// Retry initialization
AFCore.initialize(
AFCoreConfig.Builder(context = this)
.baseUrl("https://api.example.com")
.clientId("your-client-id")
.clientSecret("your-client-secret")
.build()
)
}
}
}
Clearing encrypted preferences will log the user out. They will need to re-authenticate.
9. Cache Returning Stale Data
Symptoms: API calls return outdated data even after the user performed an action that should have changed it (e.g., submitting a visit, updating profile).
Cause: The in-memory GET cache has a 15-minute TTL. If you fetch data, then mutate it via POST/PUT/DELETE, a subsequent GET within 15 minutes may return the cached (pre-mutation) response.
Solution:
- Mutations auto-invalidate: POST, PUT, and DELETE requests always skip the cache. However, related GET endpoints are not automatically invalidated.
- Use
skipCache: After a mutation, useskipCacheon the follow-up GET to force a fresh response.
- Android (Kotlin)
- iOS (Swift)
// Submit a visit (POST — skips cache automatically)
AFCore.activities().submitAutomaticVisit(locationId, date)
// Fetch updated activities with cache bypass
val freshData = AFCore.activities().get(month = 2, year = 2026, skipCache = true)
// Submit a visit (POST — skips cache automatically)
try await AFCore.activities().submitAutomaticVisit(locationId: locationId, date: date)
// Fetch updated activities with cache bypass
let freshData = try await AFCore.activities().get(month: 2, year: 2026, skipCache: true)
10. Concurrent Requests Causing Duplicates
Symptoms: You suspect that making the same API call from multiple ViewModels or views simultaneously causes duplicate network requests or duplicate server-side records.
Cause: This is not an issue with AFCore. The SDK's request coalescing mechanism (via CompletableDeferred) automatically merges concurrent identical GET requests into a single network call. All callers receive the same response.
What to verify:
- GET requests are always coalesced -- no action needed
- POST requests are not coalesced (each call makes a separate request). If you are seeing duplicate submissions, ensure your UI prevents double-taps or multiple button presses
11. Token Refresh Loop
Symptoms: Logs show repeated token refresh attempts. The app seems stuck in a cycle of refreshing tokens.
Cause: The server is returning errors on token refresh, and the SDK is retrying with exponential backoff (1s, 2s, 4s, 8s, 16s, 30s cap).
Diagnosis:
- Enable debug logging and look for
TokenManagerlog entries - Check the HTTP status codes on refresh responses:
401or403: Credentials are invalid -- the refresh token may have been revoked server-side500or503: Server issue -- the SDK will keep retrying with backoff429: Rate limited -- the SDK's backoff should resolve this
Solution:
- The exponential backoff (capped at 30 seconds) prevents true infinite loops
- If the session cannot be recovered, the SDK will eventually emit
SESSION_EXPIREDon thesessionStateflow - Listen for
SESSION_EXPIREDand prompt the user to log in again - Do not implement your own retry logic on top of the SDK's built-in backoff
12. watchOS Features Not Working
Symptoms: Geofencing, barcode scanning, or proximity detection methods throw errors or are unavailable on watchOS.
Cause: These features are not supported on watchOS due to hardware and OS limitations:
| Feature | watchOS Support | Reason |
|---|---|---|
| Geofencing | Not supported | No CLLocationManager region monitoring |
| Barcode scanning | Not supported | No camera |
| Proximity/Beacon | Not supported | No BLE beacon ranging |
| Background auto-sync | Not supported | No WorkManager or background delivery |
Solution: Check feature availability before showing UI:
// watchOS: Check feature availability
let geofencingStatus = try await AFCore.geofencing().getStatus()
if geofencingStatus.isAvailable {
showGeofencingUI() // Won't be true on watchOS
}
let hasPermissions = try await AFCore.smartWalking().hasPermissions()
// SmartWalking works on watchOS, but only during foreground sessions
On watchOS, SmartWalking syncs only occur during foreground sessions. Schedule syncs early in the session to ensure data is transmitted before the session ends.