EEltaCrew
Home › Blog

dispatchDetachedFromWindow NullPointerException: tracking an Android crash with none of my code in the stack

Published 2026-10-01Measured 2026-09-30About 3 min readelta · EltaCrew

MdViewer hit a NullPointerException whose stack trace contained only framework frames. The cause was one line that removed a sibling view inside onDetachedFromWindow, combined with how a parent ViewGroup walks its child array. How it was reproduced, why it happens and how it was fixed.

Two crash reports

On September 30, Crashlytics showed two new crash issues in MdViewer 1.0.37. Both came from a Galaxy S23 on Android 16, with the same message:

java.lang.NullPointerException: Attempt to invoke virtual method
'void android.view.View.dispatchDetachedFromWindow()' on a null object reference
    at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java)
    at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java)
    ...

The awkward part: not a single line of my code in the stack. Just framework frames, plus a misleading MainActivity.onCreate that R8 inlining had dropped in. The stack could not tell me which view was null.

Where to look instead

If dispatchDetachedFromWindow meets a null child, something changed the view tree while it was being detached. So instead of reading the stack, I searched the code for everything that runs on detach:

onDetachedFromWindow
OnAttachStateChangeListener
doOnDetach

The hit was SegmentsView, written in 1.0.36 when the document screen moved to a RecyclerView-based layout.

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    dropMermaid()   // removeView() of the sibling mermaidHost from the parent
}

MdViewer draws mermaid diagrams in a WebView. The container for that WebView, mermaidHost, sat as a sibling of the document view at index 0 of the parent CoordinatorLayout. To clean it up together with the document view, I removed it in onDetachedFromWindow. That was the bug.

Why a child becomes null

A parent ViewGroup detaches its children like this (AOSP, simplified):

final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
    children[i].dispatchDetachedFromWindow();
}

count and children are captured before the loop. When my onDetachedFromWindow, called from inside that loop, removes a sibling, the parent's child array shifts left by one and its last slot becomes null. The loop still runs to the captured count, reaches the null slot, and throws.

That is why the conditions were narrow. It crashed only when a document containing a mermaid diagram was open and the viewer was left (the fragment removes its view) or the screen was recreated, for example by switching dark mode. Documents without diagrams never created mermaidHost, so nothing happened. With the right conditions, it reproduced every time.

The fix

Move the removal to after the parent's loop has finished.

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    val m = mermaid
    val h = mermaidHost
    mermaid = null
    mermaidHost = null
    if (m != null || h != null) Handler(Looper.getMainLooper()).post {
        m?.destroy()
        h?.let { (it.parent as? ViewGroup)?.removeView(it) }
    }
}

It uses Handler(Looper.getMainLooper()).post, not View.post, on purpose. Calling View.post on a view that is already detached parks the task in the view's own queue until the view is attached again. When the user leaves the viewer for good, that never happens, and the WebView is never cleaned up. The main handler runs it right after the current traversal.

Verification

First I reproduced the crash on the build without the fix, then ran the same steps on the fixed build. On a Galaxy Note10+, with a mermaid document open:

The old build crashed as expected; the fixed build survived all four. The fix shipped in MdViewer 1.0.38.

Takeaways

The app in this post is MdViewer on Google Play, a markdown viewer for Android.

AndroidCrashViewMdViewer

Related

How to open .md files on Android when AI-generated markdown shows up as raw symbolsOpen a .md file from ChatGPT, Claude or Codex, an Obsidian note or a GitHub README on your phone and you often see raw #, * and |. Why that happens, three ways to read markdown properly on Android, and what breaks most often, from building a markdown viewer.R8 on, build green, app broken: five runtime traps that actually hit across 15 appsI enabled R8 in 15 apps for Play's 2027 code-optimization requirement. Every build passed, but 2 of the first 7 I installed broke at runtime. Gson deserialization, getIdentifier resource lookups, Room _Impl classes, and debuggable builds that silently disable R8: the kinds of failure that never show up in a crash log.Why switching language inside the app sometimes does nothing: setApplicationLocales only recreates activities that are on screen"I switched to English and it stayed Korean until I restarted the app." The cause is that AppCompat recreates only the activities that are started at that moment; screens that were stopped behind it come back in the old language. The fix I applied to 8 apps, and five traps that came with it.