How to Debug Orientation Change Bugs in Mobile Apps
How to Debug Orientation Change Bugs in Mobile Apps starts with reproducing the issue reliably and gathering the right signals. Orientation changes trigger a full recreation of the Activity (or Fragme
How to Debug Orientation Change Bugs in Mobile Apps starts with reproducing the issue reliably and gathering the right signals. Orientation changes trigger a full recreation of the Activity (or Fragment) hierarchy unless you declare android:configChanges to handle them yourself. During this cycle the system calls onSaveInstanceState, destroys the current instance, creates a new one, and restores state via onCreate or onRestoreInstanceState. Any mismatch between what is saved and what is restored, or any assumption about UI dimensions that only holds in one orientation, can surface as a crash, an unresponsive UI, missing data, or visual glitches. The following guide walks you through a repeatable process to locate, diagnose, and fix these problems, with concrete commands, logs, and a triage table you can keep on hand.
1. Understanding the Orientation Change Lifecycle
When the device rotates, the Android framework treats it as a configuration change. By default, the current Activity is destroyed and recreated. The sequence of callbacks is:
onPause()– UI is called as the activity loses foreground focus.onSaveInstanceState(Bundle outState)– you can put primitive data or references to ViewModel‑saved handles here.onStop()– activity is no longer visible.onDestroy()– the instance is fully torn down.- A new instance is created:
onCreate(Bundle savedInstanceState),onStart(),onResume(). - If you overrode
onRestoreInstanceState(Bundle savedInstanceState), it runs afteronStart().
Fragments follow a similar pattern, with their own onSaveInstanceState and onCreateView/onViewCreated calls. If you declare android:configChanges="orientation|screenSize" in the manifest, the system skips destruction and instead calls onConfigurationChanged(Configuration newConfig). In that case you must manually reload resources that depend on orientation (e.g., layouts, drawables, dimensions).
1.1 Activity vs. Fragment State Management
Activities own the window and receive the configuration change first. Fragments are notified via their host Activity. A common mistake is to store UI state only in the Activity’s Bundle and forget to propagate it to child Fragments, leading to lost selections or reset scroll positions after rotation. Conversely, storing large objects (bitmaps, cursors) in the Bundle can cause TransactionTooLargeException during the IPC transfer to the new instance.
1.2 Implicit vs. Explicit Handling
If you do not declare configChanges, the system handles recreation for you. This is the safest path because the framework guarantees that all resources are reloaded from the appropriate qualifier directories (layout-land, values-sw600dp, etc.). If you do declare configChanges, you take responsibility for:
- Reloading layouts (
setContentViewagain) if you inflated them manually. - Updating any cached dimension values obtained via
getResources().getDimension(). - Notifying adapters or custom views that the screen size changed.
Failing to do any of these leaves stale references that cause layout mis‑measurements or null‑pointer exceptions.
2. Common Root Causes of Orientation Bugs
Understanding the typical failure modes helps you focus your debugging effort. Below are the most frequent origins, each paired with a concrete symptom you will see in logs or on screen.
2.1 State Loss from Incorrect onSaveInstanceState / onRestoreInstanceState
- Symptom: After rotation, user‑entered text disappears, selected item resets to first position, or a
NullPointerExceptionoccurs when accessing a view that was expected to be non‑null. - Cause: Required fields omitted from the Bundle, or complex objects (e.g.,
Parcelablelists) not properly implemented. - Detect: Look for
java.lang.NullPointerExceptionat the line where you read fromsavedInstanceStateor where you cast a retrieved object.
2.2 UI Layout Assumptions Based on Portrait Dimensions
- Symptom: Views overlap, are clipped, or disappear entirely in landscape; sometimes the app crashes with
android.view.InflateExceptionbecause a referenced ID is missing in the layout‑land file. - Cause: Hard‑coded pixel values, reliance on
match_parentvs.wrap_contentmismatches, or missing alternate layout resources. - Detect: Use Layout Inspector to compare measured widths/heights between orientations; look for
android.view.View$MeasureSpecwarnings in Logcat.
2.3 Resource Qualifier Mismatches
- Symptom: Colors, strings, or dimensions appear wrong after rotation (e.g., a red button turns gray) or the app throws
Resources.NotFoundExceptionfor a drawable that only exists indrawable-port. - Cause: Forgetting to provide the resource in the appropriate qualifier folder (
drawable-land,values-sw720dp-land, etc.). - Detect: Search Logcat for
Unable to find resource ID #0x...and check the resource directory tree.
2.4 Threading and Async Work Not Tied to Lifecycle
- Symptom: After rotation, a background thread continues to update UI on the destroyed Activity, leading to
IllegalStateException: Fragment not attached to Activityor a silent UI freeze. - Cause: AsyncTask, RxJava subscriptions, or Kotlin coroutines launched with
activityScopethat is not cancelled inonDestroyView/onDestroy. - Detect: Look for
java.lang.IllegalStateException: Fragment ... not attached to a hostor messages likeAttempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference.
2.5 Third‑Party Library Incompatibilities
- Symptom: Crash inside a SDK callback (e.g., ad network, analytics) only when orientation changes, often with obfuscated stack traces.
- Cause: The library retains a reference to the old Activity context or assumes a fixed orientation.
- Detect: Identify the library name in the stack trace; check its documentation for orientation‑related init calls or lifecycle callbacks.
3. Building a Reliable Reproduction Matrix
A systematic test matrix lets you verify that a fix works across devices, API levels, and orientation paths (portrait→landscape, landscape→portrait, and multiple successive rotations). The table below captures the essential variables.
| Test Case ID | Description | Device/API | Rotation Path | Pre‑condition | Expected Result | How to Trigger | |
|---|---|---|---|---|---|---|---|
| TC‑01 | Simple form with EditText | Pixel 4 API 33 | Portrait → Landscape | User types “test” | Text persists after rotation | ADB shell input keyevent KEYCODE_ROTATE | |
| TC‑02 | RecyclerView with scroll position | Samsung S22 API 31 | Landscape → Portrait | List scrolled to item 150 | Same item visible at top | ADB shell input swipe 300 800 300 200 then rotate | |
| TC‑03 | Fragment with ViewModel LiveData | Emulator API 28 | Portrait → Landscape → Portrait | LiveData holds a list of 5 items | List restored with 5 items after two rotations | Rotate twice via emulator controls | |
| TC‑04 | Ad banner from third‑party SDK | OnePlus 9 API 30 | Portrait → Landscape | Banner loaded | Banner resizes, no crash | Load ad, then rotate | |
| TC‑05 | Custom view measuring based on screen width | Nexus 5X API 25 | Landscape → Portrait | Custom view width = 720px in landscape | Width recomputed to 416px in portrait | Set custom view, rotate | |
| TC‑06 | Application with `android:configChanges="orientation | screenSize"` | Pixel 3 API 32 | Portrait → Landscape | Activity declares configChanges | onConfigurationChanged called, no recreate | Rotate, check log for onConfigurationChanged |
| TC‑07 | Deep link launch then rotate | Any API ≥21 | Portrait → Landscape | App launched via URL | Activity state preserved after rotation | adb shell am start -d "yourapp://item/42" then rotate | |
| TC‑08 | Low memory killer simulation | Emulator API 30 (low RAM) | Portrait → Landscape | System kills background process | Activity restored from savedInstanceState | Use adb shell am kill then rotate |
How to use the matrix
- Automate each case with an instrumentation test (Espresso or UIAutomator) that asserts the expected result.
- Run the matrix on a device farm (Firebase Test Lab, BrowserStack) to catch device‑specific qualifier issues.
- Record failures in a spreadsheet; the “How to Trigger” column gives you a reproducible adb command or UI script you can paste into a CI step.
4. Manual Debugging Techniques
When a bug appears, start with the most immediate signals: logs, visual inspection, and forced rotation commands. The following steps are effective on both emulators and physical devices.
4.1 Using Android Studio Layout Inspector
- Run the app on a device or emulator.
- Rotate to the problematic orientation.
- Open View → Tool Windows → Layout Inspector.
- Select the frozen or mis‑placed view in the hierarchy.
- Examine its layout parameters (
width,height,margin) and compare them to the values in portrait. - If the values are stale (e.g., width still 1080px after rotating to a 720px‑wide screen), you likely missed a layout reload or are using a hard‑coded dimension.
4.2 Logcat Filters for Orientation Events
The system logs orientation changes with the tag ActivityManager. Use:
adb logcat ActivityManager:V *:S
You will see lines like:
I/ActivityManager: Display Flicker: rotation=1
To capture your own lifecycle calls, add a filter for your package:
adb logcat *:S com.example.myapp:V
Then insert Log.d("Orientation", "onSaveInstanceState: "+outState); in your callbacks. After a rotation, you can grep the saved bundle keys:
adb logcat | grep "Orientation"
If a key you expect is missing, you know the state‑saving path failed.
4.3 ADB Commands to Force Rotation
- Emulator:
adb shell settings put system user_rotation 0(portrait),1(landscape right),2(reverse portrait),3(landscape left). - Physical device (requires root or using
surfaceflinger):adb shell cmd display set-rotation 0. - Toggle auto‑rotate:
adb shell settings put system accelerometer_rotation 0to disable, then manually set with the commands above.
You can script a sequence:
#!/bin/bash
for i in {0..3}; do
adb shell cmd display set-rotation $i
sleep 2
done
This runs the app through all four orientations, useful for stress‑testing.
4.4 Emulator vs. Device Considerations
- Emulator reproduces configuration changes instantly and lets you snapshot states. Use the Extended Controls → Rotate buttons for quick checks.
- Physical device may exhibit sensor lag or OEM‑specific overrides (e.g., Samsung’s multi‑window mode). Always validate on at least one device per screen‑size bucket (small, normal, large, xlarge) and per OS version you support.
- Battery‑saver modes can throttle background services; disable them while debugging to avoid false negatives.
5. Automated Detection: Logs, Profilers, and Autonomous Exploration
Beyond manual checks, you can instrument your CI pipeline to catch orientation regressions early. The following techniques generate actionable data without human interaction.
5.1 Systrace for UI Thread Stalls
Systrace captures kernel and user‑space traces with minimal overhead. To record a rotation scenario:
python $ANDROID_SDK/platform-tools/systrace/systrace.py \
-t 10 \
-o rotation_trace.html \
sched gfx view wm
Then perform a rotation via ADB or UIAutomator. Open rotation_trace.html in Chrome and look for:
- Long frames (>16ms) in the View or WM sections.
- Any
Choreographer#doFramedelays that coincide withonCreatespikes.
If you see a gap where the UI thread is blocked for >100ms during recreation, inspect the trace for heavy work (JSON parsing, bitmap decoding) happening on the main thread inside onCreate or onRestoreInstanceState.
5.2 GPU Overdraw and Profile GPU Rendering
Enable Developer Options → Debug GPU Overdraw to see color‑coded overdraw after rotation. Excessive overdraw often indicates that layouts are being inflated twice (once for portrait, once for landscape) without clearing the previous hierarchy, which can waste memory and cause flicker.
Similarly, Profile GPU Rendering shows the time spent in each stage of the render pipeline. A jump in the Process or Execute column after rotation hints at expensive layout passes.
5.3 Firebase Test Lab Orientation Matrix
Firebase Test Lab lets you define a matrix of devices, API levels, and orientations. In your gradle file:
firebaseTestLab {
instrumentation {
targets {
all {
deviceIds.addAll(["pixel3api30", "nexus5xapi28"])
osVersionIds.addAll(["30", "28"])
locales.addAll(["en", "fr"])
// orientation is implicit; each test runs in both portrait and landscape
}
}
}
}
Run your instrumentation suite with:
./gradlew firebaseTestLabRelease
The console output will list failures per device/orientation, giving you a quick heatmap of where orientation bugs hide.
5.4 Using SUSA Autonomous Exploration (Mention SUSA)
SUSA can be pointed at an APK or a web URL and will autonomously navigate the app, generating a variety of user‑persona sessions. By default it includes a “rotator” persona that periodically triggers orientation changes while exercising core flows (login, search, checkout).
To enable orientation‑focused exploration:
pip install susatest-agent
susatest explore --app my-app.apk \
--personas rotator,curious,impatient \
--max-depth 6 \
--output-dir susa-runs/
SUSA logs each rotation event, captures any crashes or ANRs, and automatically creates regression scripts (Appium for Android, Playwright for web). Because it explores without pre‑written test cases, it often discovers orientation bugs that only appear after a non‑trivial sequence of actions (e.g., rotate → open navigation drawer → rotate again → press a button). The generated Appium script can be added to your CI as a smoke test that runs on every PR.
> Note: SUSA is mentioned here only to illustrate an automated, script‑free way to surface orientation bugs; the debugging steps that follow remain valid regardless of the tool you use.
6. Step‑by‑Step Diagnosis Workflow
When a bug is reported, follow this workflow to isolate the root cause efficiently.
6.1 Capture Crash/ANR Logs
- Reproduce the issue on a device with
adb logcatrunning. - Save the log to a file:
adb logcat -d > orientation_bug.log. - Search for
FATAL EXCEPTION,ANR in, orSignal 11. - Identify the crashing thread (usually
main) and the exact method where the exception originates.
6.2 Identify the Offending Component
From the stack trace, note:
- Class name (Activity, Fragment, custom View).
- Method (
onCreate,onRestoreInstanceState,onConfigurationChanged, a callback from a library).
If the trace points to a third‑party class, check its version and known issues. If it points to your own code, proceed to state‑management checks.
6.3 Check SavedInstanceState Handling
Open the source file for the component. Verify:
- Every piece of UI‑relevant data (text, selection, scroll position, boolean flags) is put into
outStateinonSaveInstanceState. - The corresponding get calls exist in
onCreate*or*onRestoreInstanceState. - Primitive types are used; avoid putting non‑Parcelable objects directly. If you must, wrap them in a custom
Parcelableor use a ViewModel.
If you find missing puts/gets, that is likely the cause of state loss.
6.4 Inspect Layout XML for Qualifiers
- Locate the layout file(s) used by the Activity/Fragment (
layout/,layout-land/,layout-sw600dp/). - Ensure that every
@+id/referenced in code exists in all layout variants that can be loaded at runtime. - Use Android Studio’s Split view to compare portrait and landscape files side‑by‑side.
- If a view is missing in landscape, either add it or guard the code with
if (findViewById(R.id.my_view) != null).
6.5 Verify Async Tasks and Lifecycle‑Aware Components
Search for:
AsyncTask,Thread,ExecutorService,Handler,RxJava,LiveData,Flow,CoroutineScope.- Ensure each subscription or work item is tied to a lifecycle scope (
viewLifecycleOwner.lifecycleScopefor Fragments,lifecycleScopefor Activities withLifecycleOwner). - Look for missing
cancel()ordispose()calls inonDestroyView/onDestroy.
If you discover a background job updating UI after the Activity is destroyed, that explains the illegal‑state exceptions.
6.6 Test the Fix with the Matrix
After applying a change, run the reproduction matrix (Section 3) on at least two device/API combos. Verify that previously failing test cases now pass and that no new regressions appear.
7. Fix Patterns for Each Cause
Below are concrete code snippets and configuration adjustments that address the root causes identified earlier.
7.1 Proper State Persistence
Using ViewModel (recommended)
class EditProfileViewModel : ViewModel() {
private val _name = MutableLiveData<String>()
val name: LiveData<String> = _name
fun setName(value: String) {
_name.value = value
}
}
In the Fragment:
class EditProfileFragment : Fragment(R.layout.fragment_edit_profile) {
private val viewModel: EditProfileViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
viewModel.name.observe(viewLifecycleOwner) { name ->
binding.editName.setText(name)
}
binding.editName.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
viewModel.setName(binding.editName.text.toString())
true
} else false
}
}
}
Because ViewModel survives configuration changes, you no longer need to manually save/restore the name.
Manual Bundle Approach (when ViewModel not feasible)
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_NAME, binding.editName.text.toString())
outState.putInt(KEY_SELECTED_POSITION, spinner.selectedItemPosition)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null) {
val name = savedInstanceState.getString(KEY_NAME)
binding.editName.setText(name)
spinner.setSelection(savedInstanceState.getInt(KEY_SELECTED_POSITION, 0))
}
}
7.2 Using Resource Qualifiers Correctly
Create res/values-land/dimens.xml for landscape‑specific spacing:
<!-- res/values/dimens.xml (portrait default) -->
<dimen name="item_height">56dp</dimen>
<!-- res/values-land/dimens.xml -->
<dimen name="item_height">48dp</dimen>
Then in layout:
<View
android:layout_width="match_parent"
android:layout_height="@dimen/item_height"
android:background="@color/item_bg"/>
If you need a completely different layout (e.g., a two‑pane master/detail in landscape), place it under res/layout-land/ and let the system load it automatically.
7.3 Handling Configuration Changes Manually (When Required)
Declare in manifest:
<activity
android:name=".MapActivity"
android:configChanges="orientation|screenSize|keyboardHidden"/>
Then implement:
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.mapContainer.setPadding(0, 0, 0, 0)
} else {
binding.mapContainer.setPadding(0, toolbarHeight, 0, 0)
}
// Reload any bitmap assets that depend on density
mapView.reloadMapIfNeeded()
}
Caution: Only use this approach when you have measured that the cost of fragment recreation outweighs the benefit of automatic resource reloading. Most apps should rely on the default destroy/recreate cycle.
7.4 Guarding Against UI Work on Wrong Thread
Kotlin Coroutines with lifecycleScope
class FeedViewModel : ViewModel() {
private val _items = MutableLiveData<List<Item>>()
val items: LiveData<List<Item>> = _items
fun loadItems() {
viewModelScope.launch {
try {
val result = repository.fetchItems()
_items.value = result
} catch (e: IOException) {
// handle error
}
}
}
}
The viewModelScope is automatically cancelled when the ViewModel is cleared, preventing work from leaking after rotation.
RxJava with Disposable
private val disposable = CompositeDisposable()
override fun onStart() {
super.onStart()
disposable.add(
repo.getUpdates()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { updateUI(it) }
)
}
override fun onStop() {
super.onStop()
disposable.clear()
}
7.5 Updating or Wrapping Third‑Party SDKs
- Check the SDK’s changelog for orientation‑related fixes.
- If the SDK exposes a
setContext(Context)method, call it again inonConfigurationChangedwhen you manually handle changes. - If the SDK registers a listener that holds an Activity reference, wrap it in a
WeakReferenceor use the SDK’s provided lifecycle‑aware initializer (many modern ads and analytics libraries now accept aLifecycleOwner).
Example for an ad SDK that lacks lifecycle awareness:
private lateinit var adView: BannerAdView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adView = BannerAdView(this)
adView.setAdListener(object : AdListener() {
override fun onAdLoaded() {
// safe to show
}
override fun onAdFailedToLoad(error: AdError) {
Log.e("Ads", "Load failed: $error")
}
})
adView.loadAd(adRequest)
}
override fun onDestroy() {
adView.destroy() // explicit cleanup
super.onDestroy()
}
If the SDK does not provide a destroy method, call adView.setVisibility(View.GONE) and null the reference to allow GC.
8. Prevention Checklist and Best Practices
Use this checklist during code reviews or as a pre‑release gate. Each item maps directly to a failure mode discussed earlier.
| Checklist Item | Why It Matters | How to Enforce (Tool / Process) |
|---|---|---|
All UI‑relevant state saved in onSaveInstanceState or held in a ViewModel | Prevents data loss after rotation | Android Lint detector MissingSavedStateField; unit test that rotates and asserts state |
Every findViewById/binding.x accessed after onCreate checks for null | Avoids NPE when a view is missing in alternate layout | Code review rule; SpotBugs NP_NULL_ON_SOME_PATH |
| Provide layout/resources for all qualifier combinations you support | Stops Resources.NotFoundException and mis‑measured views | Gradle task that runs aapt2 dump resources and verifies IDs exist in layout-land/ |
Background work tied to a lifecycle scope (viewModelScope, lifecycleScope, Disposable) | Stops illegal‑state exceptions and memory leaks | Detector that flags AsyncTask.execute or raw Thread.start outside a lifecycle scope |
No hard‑coded pixel dimensions; use dp/sp or dimension resources | Ensures UI scales correctly across screen sizes and orientations | Lint rule HardcodedTextSize; custom rule for setWidth/height with pixel values |
If android:configChanges is used, verify onConfigurationChanged reloads all UI-dependent resources | Prevents stale dimensions and missing UI updates | Unit test that calls onConfigurationChanged and asserts updated values |
| Third‑party libraries checked for orientation‑safe usage | Avoids crashes inside SDKs | Dependency‑check step that scans for known problematic versions (e.g., using OWASP Dependency‑Check with a custom list) |
| Automated orientation stress test in CI | Catches regressions early | Run the matrix from Section 3 on Firebase Test Lab or a local device farm as part of PR verification |
| Manual exploratory testing with a rotator persona | Finds edge cases that scripted tests miss | Use SUSA or a similar autonomous explorer with a rotation‑focused persona as a nightly job |
Mark each checklist item as PASS/FAIL in your release spreadsheet; a release is blocked if any FAIL remains.
9. Closing Takeaways
Orientation change bugs are deterministic once you understand the lifecycle contract. The most reliable defense is to treat every rotation as a potential recreation point and to guard state, UI resources, and background work accordingly. Start by reproducing the issue with a simple adb rotation command or a tester persona, then capture logs to pinpoint whether the failure originates from missing saved state, a layout‑qualifier gap, or a rogue background thread. Apply the patterns in Section 7—prefer ViewModel or SavedStateHandle for UI state, use qualifier folders for layout and dimensions, tie all async work to lifecycle scopes, and only resort to manual android:configChanges handling when you have measured a clear performance benefit. Finally, institutionalize the prevention checklist and run an automated orientation matrix on every change; this turns a flaky, hard‑to‑reproduce bug into a caught‑early regression that never reaches users. By following this workflow you will eliminate the class of bugs that only appear when the user turns their phone sideways, delivering a steadier experience across all form factors.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free