
Fix Mobile Configuration Change & Orientation Crashes
Why Configuration Changes Are a Silent Crash Factory
Every mobile developer has experienced it: you build a polished screen, test it thoroughly in portrait mode, ship it — and then the crash reports roll in. The stack traces don't point to your code. The repro steps are baffling. And then you notice the common thread: every crash happened right after the user rotated their device. Configuration change and orientation crashes remain one of the most persistent sources of production instability in mobile apps, precisely because they're easy to miss during development and devastating when they hit real users. If you're shipping Android or iOS apps without a deliberate strategy for configuration change crashes, you're leaking users through a hole you can't see.
The core problem is deceptively simple. On Android, a configuration change — screen rotation, keyboard availability, dark mode toggle, font scale adjustment, locale switch — destroys and recreates the entire Activity by default. Every piece of UI state held in memory vanishes unless you explicitly preserve it. On iOS, while UIKit doesn't destroy view controllers on rotation, the combination of size class transitions, trait collection changes, and layout updates creates equally treacherous conditions. Both platforms share a common failure mode: state that was valid in one configuration becomes null, stale, or inconsistent in another, and your app crashes before it can recover.
The Android Activity Destruction Problem
When the Android system detects a configuration change, it calls onDestroy() on the current Activity instance and creates a brand-new one. This isn't a bug — it's by design. The system assumes you've provided alternative resources for the new configuration and needs to reload everything from scratch. The problem is that most developers don't think about this lifecycle event when building features. They store UI state in Activity fields, hold references to views that get garbage-collected, and never implement onSaveInstanceState().
class ProfileActivity : AppCompatActivity() {
private var userDisplayName: String? = null // Lost on rotation
private var profileImageBitmap: Bitmap? = null // Gone forever
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profile)
if (savedInstanceState != null) {
userDisplayName = savedInstanceState.getString("display_name")
} else {
fetchProfileFromNetwork() // Re-fires unnecessarily
}
}
}The most common crash patterns on Android configuration changes include: NullPointerException from fields that aren't re-initialized after recreation, WindowManager.BadTokenException from dialogs shown on destroyed Activities, Fragment state loss when transactions are committed after onSaveInstanceState(), and IllegalStateException from trying to access views that haven't been re-inflated yet. Each of these has a specific fix, but catching them all requires systematic thinking about lifecycle boundaries.
The Android team introduced ViewModel from Architecture Components specifically to address this. A ViewModel survives configuration changes by living outside the Activity lifecycle — it's scoped to the ViewModelStoreOwner and only destroyed when the owner finishes permanently, not when it's recreated for a config change. This is the single most effective tool for eliminating rotation-related crashes on Android, but it only works when developers use it consistently.
class ProfileViewModel : ViewModel() {
private val _profile = MutableLiveData<UserProfile>()
val profile: LiveData<UserProfile> = _profile
init {
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
_profile.value = repository.fetchProfile()
}
}
}For more complex scenarios, the Android documentation on saving UI states recommends SavedStateHandle for surviving process death in addition to configuration changes. Meanwhile, the Android runtime changes guide explains how to handle config changes manually with android:configChanges — but this is generally discouraged because it prevents the system from applying your alternative resources correctly.
Jetpack Compose: A Different Beast
Compose handles configuration changes fundamentally differently from Views. Since Compose is declarative and the UI is a function of state, there's no Activity field to lose — the composable simply recomposes with whatever state you provide. But this shifts the responsibility: now you must ensure your state survives recomposition.
The key insight is that remember survives recomposition but not configuration changes, while rememberSaveable survives both. Many developers reach for remember out of habit and then discover their text field content, scroll position, or selected tab vanishes on rotation.
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
var query by rememberSaveable { mutableStateOf("") } // Survives rotation
val results by viewModel.results.collectAsState()
Column {
TextField(value = query, onValueChange = { query = it })
LazyColumn {
items(results) { result ->
ResultCard(result)
}
}
}
}The Jetpack Compose state documentation is essential reading here. Compose also introduces Configuration as a composable-local that you can observe for dark mode, locale, and screen width changes — allowing you to react to configuration changes without the destructive Activity recreation cycle at all. This is a significant architectural improvement, but it requires teams to explicitly adopt these patterns across their entire Compose surface.
iOS Trait Collections and Size Class Transitions
On iOS, orientation changes trigger trait collection updates rather than view controller destruction. The system calls viewWillTransition(to:with:) and traitCollectionDidChange(_:), allowing view controllers to adapt their layouts without losing state. This is arguably a cleaner model than Android's destroy-and-recreate approach — but it comes with its own sharp edges.
The most dangerous pattern on iOS is when developers store layout-specific computed values that become stale after rotation. A common example: calculating a frame based on view.bounds.width in viewDidLoad() and never recalculating it when the bounds change. After rotation, the stored frame doesn't match the new layout, leading to overlapping views, invisible content, or NSInternalInconsistencyException from Auto Layout constraint conflicts.
class GalleryViewController: UIViewController {
private var itemSize: CGSize = .zero // Calculated once, stale after rotation
override func viewDidLoad() {
super.viewDidLoad()
itemSize = calculateItemSize(for: view.bounds.width)
}
override func viewWillTransition(
to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator
) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { _ in
self.itemSize = self.calculateItemSize(for: size.width)
self.collectionView.collectionViewLayout.invalidateLayout()
}
}
}Apple's handling size and orientation changes documentation provides a comprehensive reference, but the practical advice is straightforward: never cache layout-dependent values without invalidating them on trait collection changes. For SwiftUI, the story is similar to Compose — views are declarative and recompute automatically, but developers must still handle state ownership correctly. @StateObject survives view updates but not the identity of the view struct itself, while @State is tied to the view's lifecycle.
Real-World Patterns That Cause Crashes
Beyond the obvious lifecycle mistakes, several real-world patterns consistently produce configuration change crashes in production. Dialog management is perhaps the most insidious. On Android, if you show a DialogFragment or AlertDialog just before a rotation, the dialog's parent Activity is destroyed during the transition. If the dialog's callback references the old Activity or its fields, you get a crash. The fix is straightforward: always use viewLifecycleOwner for dialog interactions, and cancel pending dialogs in onStop().
Resource-heavy operations are another minefield. Loading a large bitmap or making a network request in an Activity without scoping it to a ViewModel or LifecycleCoroutineScope means the work continues even after the Activity is destroyed. When the callback fires on the destroyed Activity, you get a crash — and worse, you've wasted bandwidth and memory on a result nobody will see. The ViewModel overview provides the canonical solution, but the principle extends to any long-running operation.
On iOS, UIDocumentInteractionController, UIActivityViewController, and other presentation-based APIs are particularly fragile during orientation changes. Presenting a share sheet while the device rotates can trigger UIApplicationInvalidInterfaceOrientation exceptions. The mitigation is to dismiss these controllers in viewWillTransition and re-present them in the coordinator's animation block if needed.
Monitoring Configuration Change Crashes in Production
Even with perfect development practices, configuration change crashes will slip through. The combinatorial explosion of device × OS version × screen size × user behavior means you can't test every scenario. This is where production crash monitoring becomes non-negotiable. A robust crash reporting tool captures the full configuration context — device orientation at crash time, screen density, font scale, dark mode state — so you can correlate crashes with specific configuration transitions.
BugsPulse captures this configuration metadata automatically for both Android and iOS. When a crash occurs during or immediately after a rotation, you see the orientation change in the timeline alongside the crash stack trace. This is dramatically more useful than a raw stack trace in logcat, because it answers the question every developer asks first: "What was the user doing right before this crash?" Configuration change crashes often produce seemingly unrelated stack traces — a NullPointerException in a layout method doesn't tell you the user just rotated their phone, but the configuration context does.
For teams already instrumenting their apps, pairing crash reporting with session replay closes the feedback loop entirely. You can watch the exact sequence of user interactions leading up to a configuration change crash, including the rotation gesture, the screen transition, and the moment of failure. This eliminates the guesswork from reproduction and cuts mean time to resolution dramatically.
Building a Configuration-Change-Resilient Architecture
The best defense against configuration change crashes is an architecture that makes the problem category disappear. On Android, this means adopting a unidirectional data flow where all state lives in ViewModel instances that survive configuration changes, and the Activity or Fragment is a passive observer. Data flows from ViewModel to UI via LiveData, StateFlow, or SharedFlow, and user actions flow back through the ViewModel's methods. The Activity owns no meaningful state — it's just a rendering surface.
@AndroidEntryPoint
class DashboardFragment : Fragment() {
private val viewModel: DashboardViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
val binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
return binding.root
}
// Fragment holds zero state — rotation is transparent
}On iOS, the equivalent pattern is to keep state in reference types managed by a dedicated controller or coordinator, and make view controllers as thin as possible. The coordinator pattern, popularized in the iOS community, separates navigation logic from view controller lifecycle, which inherently reduces the surface area for configuration-related bugs.
For both platforms, the emerging consensus from developers across the ecosystem is that declarative UI frameworks — Jetpack Compose and SwiftUI — eliminate entire categories of configuration change bugs by construction. When the UI is a pure function of state, there's no Activity field to lose and no stale frame to reference. The investment in migrating to these frameworks pays for itself in reduced crash rates, particularly for configuration-sensitive user flows.
Testing Configuration Changes Before They Reach Users
Automated testing for configuration changes is systematically underinvested in. Most teams run UI tests in a single orientation, on a single device profile. Adding a rotation or dark mode toggle to your test suite catches an enormous number of otherwise-escaped bugs with minimal additional infrastructure.
On Android, Espresso supports device rotation in tests via UiDevice.setOrientationLeft() and UiDevice.setOrientationNatural(). A simple pattern is to perform an action in portrait, rotate, and verify the action's result persists in landscape — then rotate back and verify again. This catches state loss, null references, and layout crashes in a single test.
@Test
fun searchQuery_survivesRotation() {
onView(withId(R.id.search_input)).perform(typeText("configuration"))
uiDevice.setOrientationLeft()
onView(withId(R.id.search_input)).check(matches(withText("configuration")))
uiDevice.setOrientationNatural()
onView(withId(R.id.search_input)).check(matches(withText("configuration")))
}On iOS, XCUITest provides XCUIDevice.shared.orientation for rotation testing. Combine this with XCUIApplication state queries to verify that UI state persists across orientation changes. The key insight is that these tests are cheap to add and disproportionately effective — a single orientation test can catch bugs that would otherwise require complex manual reproduction steps.
Go Beyond Crash Reporting
Configuration change crashes are a solved category of bugs. The tools exist, the patterns are documented, and the testing infrastructure is available. What separates teams that ship stable apps from those that don't is whether they've operationalized these practices into their development workflow and their production monitoring stack.
BugsPulse gives you the configuration-aware crash reporting, session replay, and real-time alerting you need to catch orientation and configuration change crashes before your users report them. See every crash with full device context — orientation, dark mode state, font scale, screen density — and replay the exact session that led to it. Stop guessing and start fixing. Create your free account and ship with confidence.