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

February 27, 2026 · 15 min read · Common Issues

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:

  1. onPause() – UI is called as the activity loses foreground focus.
  2. onSaveInstanceState(Bundle outState) – you can put primitive data or references to ViewModel‑saved handles here.
  3. onStop() – activity is no longer visible.
  4. onDestroy() – the instance is fully torn down.
  5. A new instance is created: onCreate(Bundle savedInstanceState), onStart(), onResume().
  6. If you overrode onRestoreInstanceState(Bundle savedInstanceState), it runs after onStart().

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:

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

2.2 UI Layout Assumptions Based on Portrait Dimensions

2.3 Resource Qualifier Mismatches

2.4 Threading and Async Work Not Tied to Lifecycle

2.5 Third‑Party Library Incompatibilities

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 IDDescriptionDevice/APIRotation PathPre‑conditionExpected ResultHow to Trigger
TC‑01Simple form with EditTextPixel 4 API 33Portrait → LandscapeUser types “test”Text persists after rotationADB shell input keyevent KEYCODE_ROTATE
TC‑02RecyclerView with scroll positionSamsung S22 API 31Landscape → PortraitList scrolled to item 150Same item visible at topADB shell input swipe 300 800 300 200 then rotate
TC‑03Fragment with ViewModel LiveDataEmulator API 28Portrait → Landscape → PortraitLiveData holds a list of 5 itemsList restored with 5 items after two rotationsRotate twice via emulator controls
TC‑04Ad banner from third‑party SDKOnePlus 9 API 30Portrait → LandscapeBanner loadedBanner resizes, no crashLoad ad, then rotate
TC‑05Custom view measuring based on screen widthNexus 5X API 25Landscape → PortraitCustom view width = 720px in landscapeWidth recomputed to 416px in portraitSet custom view, rotate
TC‑06Application with `android:configChanges="orientationscreenSize"`Pixel 3 API 32Portrait → LandscapeActivity declares configChangesonConfigurationChanged called, no recreateRotate, check log for onConfigurationChanged
TC‑07Deep link launch then rotateAny API ≥21Portrait → LandscapeApp launched via URLActivity state preserved after rotationadb shell am start -d "yourapp://item/42" then rotate
TC‑08Low memory killer simulationEmulator API 30 (low RAM)Portrait → LandscapeSystem kills background processActivity restored from savedInstanceStateUse adb shell am kill then rotate

How to use the matrix

  1. Automate each case with an instrumentation test (Espresso or UIAutomator) that asserts the expected result.
  2. Run the matrix on a device farm (Firebase Test Lab, BrowserStack) to catch device‑specific qualifier issues.
  3. 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

  1. Run the app on a device or emulator.
  2. Rotate to the problematic orientation.
  3. Open View → Tool Windows → Layout Inspector.
  4. Select the frozen or mis‑placed view in the hierarchy.
  5. Examine its layout parameters (width, height, margin) and compare them to the values in portrait.
  6. 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

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

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:

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

  1. Reproduce the issue on a device with adb logcat running.
  2. Save the log to a file: adb logcat -d > orientation_bug.log.
  3. Search for FATAL EXCEPTION, ANR in, or Signal 11.
  4. 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:

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:

  1. Every piece of UI‑relevant data (text, selection, scroll position, boolean flags) is put into outState in onSaveInstanceState.
  2. The corresponding get calls exist in onCreate *or* onRestoreInstanceState.
  3. Primitive types are used; avoid putting non‑Parcelable objects directly. If you must, wrap them in a custom Parcelable or use a ViewModel.

If you find missing puts/gets, that is likely the cause of state loss.

6.4 Inspect Layout XML for Qualifiers

  1. Locate the layout file(s) used by the Activity/Fragment (layout/, layout-land/, layout-sw600dp/).
  2. Ensure that every @+id/ referenced in code exists in all layout variants that can be loaded at runtime.
  3. Use Android Studio’s Split view to compare portrait and landscape files side‑by‑side.
  4. 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:

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

  1. Check the SDK’s changelog for orientation‑related fixes.
  2. If the SDK exposes a setContext(Context) method, call it again in onConfigurationChanged when you manually handle changes.
  3. If the SDK registers a listener that holds an Activity reference, wrap it in a WeakReference or use the SDK’s provided lifecycle‑aware initializer (many modern ads and analytics libraries now accept a LifecycleOwner).

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 ItemWhy It MattersHow to Enforce (Tool / Process)
All UI‑relevant state saved in onSaveInstanceState or held in a ViewModelPrevents data loss after rotationAndroid Lint detector MissingSavedStateField; unit test that rotates and asserts state
Every findViewById/binding.x accessed after onCreate checks for nullAvoids NPE when a view is missing in alternate layoutCode review rule; SpotBugs NP_NULL_ON_SOME_PATH
Provide layout/resources for all qualifier combinations you supportStops Resources.NotFoundException and mis‑measured viewsGradle 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 leaksDetector that flags AsyncTask.execute or raw Thread.start outside a lifecycle scope
No hard‑coded pixel dimensions; use dp/sp or dimension resourcesEnsures UI scales correctly across screen sizes and orientationsLint rule HardcodedTextSize; custom rule for setWidth/height with pixel values
If android:configChanges is used, verify onConfigurationChanged reloads all UI-dependent resourcesPrevents stale dimensions and missing UI updatesUnit test that calls onConfigurationChanged and asserts updated values
Third‑party libraries checked for orientation‑safe usageAvoids crashes inside SDKsDependency‑check step that scans for known problematic versions (e.g., using OWASP Dependency‑Check with a custom list)
Automated orientation stress test in CICatches regressions earlyRun 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 personaFinds edge cases that scripted tests missUse 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