dispatchDetachedFromWindow NullPointerException: tracking an Android crash with none of my code in the stack
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:
- leave the viewer with the in-app back button
- system back
- recreate the screen twice by switching dark mode
- open the same document again
The old build crashed as expected; the fixed build survived all four. The fix shipped in MdViewer 1.0.38.
Takeaways
- Inside
onDetachedFromWindow, clean up only the view itself. Removing a sibling or another child of the parent disturbs the array the parent is walking. - For view crashes with none of your code in the stack, grepping detach hooks (
onDetachedFromWindow,doOnDetach) is faster than reading the trace. - To defer detach-time cleanup, post to the main
Handler, notView.post. - I missed this because, after changing the screen structure in 1.0.36, I never tested "leave the viewer with a mermaid document open". After a view-structure change, screens with special content need a leave-and-recreate test too.
The app in this post is MdViewer on Google Play, a markdown viewer for Android.