Testing Mobile Application Compatibility Across OS Versions
Supporting iOS 15 and Android 10 means not just "doesn't crash" — means all features work correctly, without UI artifacts, without silent failures where new API would error but old just does nothing. Gap between minSdkVersion 26 and compileSdkVersion 35 is 8 years of Android API evolution. Miss one deprecated replacement — and on Android 10 in production crashes with NoSuchMethodError.
Version Matrix: How to Choose
Testing on each OS version is irrational. Principle:
| Priority | iOS Versions | Android Versions |
|---|---|---|
| Mandatory | Current − 1 (iOS 17, 18) | Android 12, 13, 14 (API 31–34) |
| Important | minDeploymentTarget (iOS 15) | minSdkVersion (API 26–28) |
| By Analytics | Versions with >5% share in your audience | Same |
Firebase or Mixpanel analytics by os_version gives real picture. If 8% users on iOS 15 — test. If 0.3% on iOS 14 — no.
Deprecated API: Where to Catch Issues
Android
Problems with deprecated API most often in these areas:
Notifications (API 26+). On Android 8+ all notifications require NotificationChannel. Without it notify() silently ignored. App thinks notification shown — nope.
// Check: create channel only on API 26+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_ID, "General", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(channel)
}
Permissions (API 33+). READ_EXTERNAL_STORAGE on Android 13+ replaced with granular: READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO. Requesting old permission on Android 13 doesn't give media access.
Foreground Service (API 34+). Android 14 requires specifying foreground service type: dataSync, mediaPlayback, location etc. Without type — SecurityException on start.
Tool for checking: lint with NewApi rule. In Android Studio: Analyze → Inspect Code. Flags:
android {
lint {
abortOnError = true
error += setOf("NewApi", "InlinedApi")
}
}
NewApi — API call available above minSdkVersion without @RequiresApi or Build.VERSION.SDK_INT check. Auto-finds most compatibility issues before run.
iOS
@available and #available — mandatory patterns:
if #available(iOS 16.0, *) {
// NavigationStack, available from iOS 16
NavigationStack { ... }
} else {
NavigationView { ... } // deprecated, but works until iOS 15
}
Compiler warns about new API use without @available check — caught statically. But nuance: warning, not error. In large codebases such warnings get lost.
Most common miss: SwiftUI components added in new iOS versions. ContentUnavailableView (iOS 17), NavigationStack (iOS 16), Charts (iOS 16) — without fallback app crashes on iOS 15 with dyld: Symbol not found.
Tool: Xcode Simulator with Specific Versions
Download additional runtimes: Xcode → Settings → Platforms → + → iOS 15.x Simulator Runtime. After download create simulator of needed version and test on it.
Testing Process
Don't run entire E2E suite on each version — overkill. Differentiated approach:
- Smoke test on minimum supported version: main flows work, app starts.
- Full regression on current OS version.
- Point testing on intermediate versions — only functions using specific version API.
Keep list of "risky" functions by version in document: function → minimum version → tested on.
Detecting Incompatibilities Without Devices
Firebase Test Lab — testing on virtual devices with different API levels. Fast, cheap, covers most incompatibilities. For finer issues (custom Samsung ROMs, MediaTek vs Qualcomm) — real devices.
Static analysis — lint for Android, Xcode Build & Analyze for iOS. Run in CI on each PR.
Timeline
2–3 days — compiling version matrix from analytics, testing on priority versions, static analysis on deprecated API, report with incompatibility matrix. Cost is calculated individually.







