How to Debug Focus Order Issues in Mobile Apps

How to Debug Focus Order Issues in Mobile Apps starts with understanding what focus order means and why it breaks. Focus order defines the sequence in which accessibility services move focus when a us

March 02, 2026 · 16 min read · Common Issues

How to Debug Focus Order Issues in Mobile Apps starts with understanding what focus order means and why it breaks. Focus order defines the sequence in which accessibility services move focus when a user navigates with directional controls, a keyboard, or switch access. When the order is illogical, users can become lost, miss critical controls, or be unable to complete a task. This guide gives you a repeatable process to uncover, diagnose, and fix focus‑order problems in Android and iOS apps, with concrete commands, sample code, and a triage table you can keep on hand.

How to Debug Focus Order Issues in Mobile Apps: Understanding Focus Order and Its Importance

What is focus order?

Focus order is the linear path that accessibility services (TalkBack on Android, VoiceOver on iOS) follow when a user swipes left/right, presses Tab, or uses a switch. It is derived from the view hierarchy: each focusable element receives an android:focusable flag (or isAccessibilityElement on iOS) and a position determined by its layout bounds. The system sorts elements by their top‑left coordinates, then by y‑ascending, x‑ascending unless a developer overrides the order with android:accessibilityTraversalBefore / android:accessibilityTraversalAfter (Android) or UIAccessibilityCustomAction / shouldGroupAccessibilityChildren (iOS). Mis‑ordered XML, programmatic focus changes, or custom view implementations can disrupt this natural sort.

Why it matters for accessibility and UX

A broken focus order violates WCAG 2.1 Success Criterion 2.4.3 (Focus Order) and can cause:

Beyond compliance, a logical focus order improves task completion rates for power users and reduces support calls related to “I can’t find the next button.”

Common symptoms in mobile apps

Recognizing these patterns early helps you isolate whether the problem is structural (layout order) or behavioral (runtime focus changes).

How to Debug Focus Order Issues in Mobile Apps: Reproducing Issues Reliably

Manual testing with TalkBack/VoiceOver

  1. Enable TalkBack (Android Settings → Accessibility → TalkBack) or VoiceOver (iOS Settings → Accessibility → VoiceOver).
  2. Navigate the target screen using swipe gestures (TalkBack) or finger drag (VoiceOver).
  3. Listen for the spoken order; note any jumps, repeats, or missing elements.
  4. For keyboard testing, connect a USB‑C or Bluetooth keyboard and press Tab repeatedly, watching the focus highlight.

Record a short video of the session; the visual trace combined with audio makes it easier to pinpoint the exact view where order deviates.

Using adb commands to dump focus

Android provides a shell command to query the current accessibility focus:


adb shell dumpsys accessibility | grep -A2 "Focused View"

To capture the entire focus tree:


adb shell dumpsys accessibility > focus_dump.txt

The dump lists each view with its focused=true/false flag and accessibility traits. Compare dumps before and after a navigation action to see which view incorrectly gained focus.

On iOS, use the Accessibility Inspector in Xcode (Open → Developer Tools → Accessibility Inspector) and enable “Show Focus Ring” to see the focused element highlighted.

Creating a test matrix (table)

A systematic matrix helps you cover variations such as screen orientation, input method, and dynamic UI states.

Test IDNavigation MethodScreen StateOrientationExpected Focus PathObserved PathPass/Fail
T1TalkBack swipeInitial loadPortraitHeader → Username → Password → LoginHeader → Footer → Username → Password → LoginFail
T2Keyboard TabAfter loginLandscapeWelcome → Profile → SettingsWelcome → Settings → ProfileFail
T3Switch AccessDialog openPortraitDialog Title → Cancel → OKDialog Title → Background → Cancel → OKFail
T4VoiceOver dragScrolling listPortraitItem 0 → Item 1 → Item 2Item 0 → Item 2 → Item 1Fail

Populate the matrix during exploratory testing; each failure becomes a concrete bug report with steps to reproduce.

How to Debug Focus Order Issues in Mobile Apps: Tools and Signals

Android Studio Layout Inspector

The Layout Inspector shows the view tree with bounding rectangles. Enable “Show layout bounds” to see the exact coordinates used for focus sorting. Right‑click a view and select “Show in hierarchy” to verify its parent‑child ordering. If a view appears visually after another but sits higher in the tree, the focus order will be wrong.

Accessibility Scanner

Google’s Accessibility Scanner (available as a standalone app or via adb shell am start -n com.google.android.apps.accessibilityscanner/.AccessibilityScannerActivity) highlights missing content descriptions, low contrast, and potential focus‑order issues. It flags views that are focusable but not logically ordered according to their screen position.

SUSATest autonomous exploration (mention SUSA)

SUSATest can be pointed at an APK or a web URL and will explore the app using a variety of user personas. During each run it records accessibility events and builds a focus‑order graph. When the graph contains cycles or unexpected jumps, SUSATest surfaces a “Focus Order Anomaly” finding with a screenshot and the offending view IDs. Because SUSATest repeats exploration across sessions, it learns which areas are stable and which are flaky, reducing false positives over time.

Logging focus changes via accessibility events

You can register an AccessibilityService (or use adb shell service call accessibility 12 to listen) to log TYPE_VIEW_FOCUSED events:


class FocusLogger : AccessibilityService() {
    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        if (event?.eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
            val source = event.source
            source?.let {
                Log.d("FocusLogger", "Focused: ${it.className} id=${it.viewIdResourceName} bounds=${it.bounds}")
            }
            source?.recycle()
        }
    }
}

Start the service with adb shell am startservice -n com.example.app/.FocusLogger and filter logcat for FocusLogger. The timestamped log lets you correlate a user action (e.g., button press) with an unexpected focus shift.

Profilers and traces

Android Studio CPU Profiler can capture method traces that show when requestFocus() or clearFocus() is called. Look for spikes in ViewRootImpl#dispatchWindowFocusChanged or InputMethodManager#showSoftInput. On iOS, Instruments’ “Core Animation” trace reveals when the system updates the focus ring, helping you detect late‑stage focus changes caused by asynchronous layout passes.

How to Debug Focus Order Issues in Mobile Apps: Step‑by‑Step Diagnosis Workflow

Phase 1: Gather baseline

Phase 2: Isolate the scope

Phase 3: Identify offending views

  1. XML order: Is it declared before or after the view that should precede it?
  2. Traversal IDs: Does it have android:accessibilityTraversalBefore/After set incorrectly?
  3. Runtime focus: Does the view’s onFocusChangeListener or requestFocus() get called from a lifecycle method (e.g., onResume)?
  4. Custom view overrides: Does it override onInitializeAccessibilityNodeInfo and inadvertently set setFocusable(false) or reorder children?

Phase 4: Validate fix

How to Debug Focus Order Issues in Mobile Apps: Common Root Causes and Fixes

Misordered XML hierarchy

The most frequent cause is a layout file where a later‑declared view appears visually above an earlier one due to ConstraintSet biases, android:layout_alignParentTop, or negative margins. Fix by reordering the XML to match visual reading order, or add explicit traversal IDs:


<!-- Before -->
<Button
    android:id="@+id/btnCancel"
    android:layout_alignParentTop="true"
    ... />
<EditText
    android:id="@+id/etEmail"
    android:layout_below="@id/btnCancel"
    ... />

<!-- After -->
<EditText
    android:id="@+id/etEmail"
    android:layout_alignParentTop="true"
    ... />
<Button
    android:id="@+id/btnCancel"
    android:layout_below="@id/etEmail"
    ... />

If reordering breaks constraints, use android:accessibilityTraversalBefore="@id/etEmail" on the Cancel button to force the correct sequence without moving the view.

Programmatic focus changes

Developers sometimes call editText.requestFocus() in onStart to show the keyboard immediately. If this call occurs after a dialog has been shown, focus may jump from the dialog to the underlying field. Ensure focus requests are scoped to the appropriate window:


if (!isDialogShowing) {
    usernameEditText.requestFocus()
}

Alternatively, use Window.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, ...) on the dialog to prevent background focus changes.

Custom views overriding onCreateAccessibilityNodeInfo

A custom View that does not call super.onCreateAccessibilityNodeInfo(info) can erase the default focusable flag. Always chain to the super implementation and then add custom traits:


override fun onCreateAccessibilityNodeInfo(info: AccessibilityNodeInfoCompat) {
    super.onCreateAccessibilityNodeInfo(info)
    info.setClickable(true)
    info.setContentDescription("Custom toggle")
    // Do NOT set setFocusable(false) unless intentionally non‑focusable
}

Dialogs and popups stealing focus

When a dialog appears, the framework should move focus to the first focusable element inside it. If the dialog’s root layout has android:focusableInTouchMode="false" or the dialog is not declared as an alert window (type=application), focus may stay behind. Fix by:

RecyclerView item focus issues

RecyclerView recycles item views, and if you call itemView.requestFocus() inside onBindViewHolder based on a mutable state (e.g., selected position), focus may flicker as views rebound. Instead, maintain a single selectedPosition variable and call requestFocus() only when the bound holder matches that position, using Handler.post { ... } to defer until layout pass:


override fun onBindViewHolder(holder: VH, position: Int) {
    holder.bind(items[position])
    if (position == selectedPosition) {
        holder.itemView.post { holder.itemView.requestFocus() }
    }
}

WebView focus traps

A WebView that loads a page with its own tab order can trap focus inside the web content, preventing escape to native controls. Enable setFocusable(true) on the WebView and override onKeyPreIme to intercept the Escape key and move focus back:


webView.setOnKeyListener { v, keyCode, event ->
    if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_DOWN) {
        // Move focus to the first native focusable view after the WebView
        v.clearFocus()
        findViewById<R.id.firstNativeView>.requestFocus()
        return@setOnKeyListener true
    }
    false
}

(Optional) Additional cause: Dynamic view insertion

Views added at runtime (e.g., via ViewStub or fragment transactions) may be inserted with default focusable=true but placed at the end of the hierarchy, causing them to be read last. Use addView(view, index) to insert at the correct position, or call view.bringToFront() after insertion and then explicitly request focus on the intended next element.

How to Debug Focus Order Issues in Mobile Apps: Automated Detection Strategies

Unit tests with AccessibilityTestFramework

AndroidX provides AccessibilityTestFramework that lets you assert focus order:


@Test
fun `focus order matches expected sequence`() {
    val scenario = launchFragmentInContainer<LoginFragment>()
    scenario.onFragment { fragment ->
        val root = fragment.requireView()
        val order = AccessibilityChecks.getFocusOrder(root)
        assertEquals(listOf(R.id.username, R.id.password, R.id.loginBtn), order)
    }
}

If the order deviates, the test fails early in the CI pipeline.

Espresso accessibility checks

Espresso includes AccessibilityChecks.enable() which runs checks on each view action. Add a rule to fail on focus‑order violations:


@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()

@Before
fun setup() {
    AccessibilityChecks.enable()
}

When an Espresso test performs a click or type, the checker logs any accessibility issue, including “Focus order does not match reading order.”

UIAutomator scripts

For cross‑app scenarios (e.g., testing that a share intent does not steal focus), UIAutomator can navigate using accessibility IDs and assert the focused element:


UiObject2 focus = device.findObject(By.clazz("android.widget.EditText")
                                   .text("Username"));
assertTrue(focus.isFocused());
// Trigger action that opens a dialog
device.findObject(By.res("com.example.app", "id/share_btn")).click();
UiObject2 dialogTitle = device.findObject(By.text("Share via"));
assertTrue(dialogTitle.isFocused()); // Expect focus inside dialog

SUSATest regression script generation (mention SUSA)

After an exploratory run, SUSATest can export the discovered flow as an Appium (Android) or Playwright (Web) script. The script includes explicit await page.focus('#username') calls, preserving the exact focus sequence observed. Commit these scripts to your repository; subsequent SUSATest runs compare the live focus order against the script and flag divergences as regressions.

CI integration

Add a step that runs the accessibility test suite on every pull request:


# .github/workflows/accessibility.yml
name: Accessibility CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          distribution: temurin
          version: '17'
      - name: Run tests
        gradlew: ./gradlew connectedDebugAndroidTest

If any focus‑order test fails, the workflow blocks merge, preventing regressions from reaching production.

How to Debug Focus Order Issues in Mobile Apps: Preventing Focus Order Regressions

Code review checklist

Add a dedicated item to your pull‑request template:

Reviewers can quickly scan for these items, reducing the chance of oversight.

Automated lint rules

Create a custom lint detector that flags views with android:accessibilityTraversalBefore or After that point to a view declared later in the same file (potentially indicating a workaround for misordering). Also flag any requestFocus() call not inside a listener:


class FocusOrderDetector : Detector(), SourceCodeScanner {
    override fun createPsiElementVisitor(context: JavaContext): PsiElementVisitor {
        return object : JavaElementVisitor() {
            override fun visitMethodCallExpression(expression: PsiMethodCallExpression) {
                if (expression.methodExpression.referencedMethod?.name == "requestFocus") {
                    // Check if inside a listener
                    val containingClass = PsiTreeUtil.getParentOfType(expression, PsiClass::class.java)
                    if (containingClass == null || !hasListenerAnnotation(containingClass)) {
                        context.report(ISSUE, expression, context.getLocation(expression),
                                "Avoid programmatic requestFocus outside of user‑initiated callbacks")
                    }
                }
            }
        }
    }
}

Register the detector in your lint.xml and run ./gradlew lintDebug as part of CI.

Design system guidelines

Document the expected focus order for common components (e.g., form fields, bottom navigation, modal dialogs) in your design system. Provide XML snippets that enforce the order, and encourage designers to annotate mockups with focus‑flow arrows. When developers implement a component, they can copy the prescribed snippet, guaranteeing consistency.

Using Jetpack Compose semantics

In Compose, focus order follows the composition order unless you modify it with focusOrder() modifier. Ensure that any conditional UI (e.g., if (showError) Text(...)) does not reorder composables unexpectedly. Use focusRequester and requestFocus() inside LaunchedEffect tied to a state change, not directly in the composable body:


var emailFocus by remember { mutableStateOf(false) }
val emailRequester = remember { FocusRequester() }

OutlinedTextField(
    value = email,
    onValueChange = { email = it },
    label = { Text("Email") },
    modifier = Modifier
        .focusRequester(emailRequester)
        .onFocusChanged { emailFocus = it.isFocused }
)

LaunchedEffect(emailFocus) {
    if (emailFocus) {
        emailRequester.requestFocus()
    }
}

This pattern prevents focus from being requested before the composition is laid out, avoiding timing‑related bugs.

Training and awareness

Run a short workshop for developers and QA on using TalkBack/VoiceOver and interpreting accessibility logs. Provide a cheat sheet with the most common adb commands and a focus‑order test matrix template. When the whole team shares a mental model of how focus works, issues are caught earlier in design reviews rather than after release.

How to Debug Focus Order Issues in Mobile Apps: Real‑World Case Studies

E‑commerce app checkout flow

A checkout screen contained a coupon field, a “Apply” button, then a list of shipping options. Users reported that after typing a coupon, TalkBack jumped from the coupon field to the “Place Order” button, skipping the shipping list. Inspection revealed that the shipping list was wrapped in a NestedScrollView with android:fillViewport="true" causing its height to match the parent, pushing it visually below the button but keeping it earlier in the XML. Fix: reorder the XML so the NestedScrollView precedes the button, or add android:accessibilityTraversalBefore="@id/placeOrderButton" on the list. After the change, TalkBack read coupon → Apply → shipping options → Place Order, matching the visual flow and reducing abandoned carts by 4%.

Banking app login screen

The login screen had a username field, password field, a “Show password” toggle, and a “Forgot password?” link positioned below the button. TalkBack users said the link was read before the password field, causing confusion. The root cause was a custom ToggleButton that overrode onCreateAccessibilityNodeInfo and called setFocusable(false) on its child ImageView, inadvertently marking the entire toggle as non‑focusable. The framework then skipped to the next focusable view, which was the link. Fix: remove the setFocusable(false) call and rely on the toggle’s default focusable behavior. Post‑fix, the spoken order matched the visual layout, and support tickets related to login dropped by 30%.

Social media feed with infinite scroll

A feed implemented with a RecyclerView used a RecyclerView.OnScrollListener to load more items. When new items arrived, the adapter called requestFocus() on the first newly bound view to keep the keyboard visible for a comment bar at the bottom. However, the request occurred during the layout pass, causing focus to jump from the comment bar to the new item, making it seem as if the comment disappeared. Fix: moved the focus request inside a Handler.postAtFrontOfQueue that executed after the layout pass, ensuring the comment bar retained focus when appropriate. Automated accessibility tests now assert that focus never leaves the comment bar unless the user explicitly taps elsewhere.

How to Debug Focus Order Issues in Mobile Apps: Edge Cases that Appear Only in Production

Dynamic font scaling

Users who enable large font sizes may cause views to reflow, changing their vertical positions and thus the natural focus order. A view that was originally above another may wrap and end up below it after scaling. Test with adb shell settings put system font_scale 1.3 and re‑run the TalkBack swipe sequence. If order changes, consider using android:accessibilityTraversalBefore/After to lock the order regardless of layout shifts, or avoid relying on implicit order for critical flows.

Multi‑window mode

In split‑screen or free‑form windows, the system may treat the app’s window as partially obscured, altering which views are considered “visible” for focus ordering. A view that is hidden behind the system bar may still be focusable, causing TalkBack to read it before visible controls. Use adb shell am stack list to verify the windowing mode and test with adb shell am resize display 0 1080x1920 (simulate portrait split). Ensure that any view that becomes invisible due to window constraints also sets android:importantForAccessibility="no" or is removed from the hierarchy.

External keyboards and switch access

When a hardware keyboard is attached, focus can move via arrow keys that bypass touch‑based hit testing. Some custom views consume key events in onKeyDown without calling super, preventing focus from moving. Verify that your views either return false for unhandled keys or invoke super.onKeyDown. For switch access, ensure that android:accessibilityTraversalBefore/After is respected; some switch‑access implementations ignore XML order and rely solely on screen coordinates, so a view with a negative translationY may be skipped.

Localization (RTL) effects

In right‑to‑left languages, the framework mirrors horizontal coordinates but retains vertical ordering. A view that uses android:layout_alignParentEnd may appear on the left in LTR but on the right in RTL, altering the expected left‑to‑right reading order. Test with adb shell setprop persist.sys.locale ar-EG and run TalkBack. If the order feels reversed, add explicit android:accessibilityTraversalBefore/After attributes that are locale‑agnostic (based on view IDs rather than positional assumptions).

How to Debug Focus Order Issues in Mobile Apps: Quick Reference Checklist and Takeaways

Diagnostic checklist

Fix verification steps

  1. Apply the candidate fix (XML reorder, remove erroneous focus call, add traversal IDs, etc.).
  2. Re‑run the TalkBack/VoiceOver swipe and confirm spoken order matches expectation.
  3. Run the automated accessibility test suite (Espresso/UIAutomator) – no new failures.
  4. Add or update a test matrix row to reflect the pass.
  5. Commit the change with a comment referencing the focus‑order issue ID.

Prevention checklist

Final takeaways

Focus order is a deterministic property of the view hierarchy, but it is easily disturbed by layout quirks, runtime focus calls, and custom view implementations. By combining manual verification with TalkBack/VoiceOver, automated dumps, lint rules, and targeted Espresso tests, you can turn an elusive UX bug into a repeatable, fixable regression. Treat focus order as a first‑class UI contract: document it, test it, and guard it against changes just as you would any visual or functional requirement. The payoff is fewer accessibility complaints, smoother keyboard and switch‑access navigation, and a product that reliably serves all users.

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