ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-05-16Intermediate

RecyclerView IndexOutOfBoundsException: The Defensive Copy Fix Antigravity Initially Missed

After 50+ RecyclerView crashes appeared in 28 days post-release, here's how I used Antigravity to debug the issue — and why the first suggestion missed the real cause.

android28recyclerview2crash2debugging15antigravity435

After shipping v2.0.0 of my Android wallpaper app, Beautiful HD Wallpapers, I watched the same IndexOutOfBoundsException stack trace accumulate in Google Play Console — 50+ crashes over 28 days. When I asked Antigravity to help debug it, the first suggestion was wrong. Here's how we got to the real fix.

What the Crash Looked Like

The error appearing in Android Vitals was almost always the same:

java.lang.IndexOutOfBoundsException: Inconsistency detected.
Invalid item position 12(offset:12).state:13

The numbers varied slightly each time, but the pattern was consistent: at the moment RecyclerView tried to render, the number of items the adapter expected didn't match the actual list size.

Running a portfolio of apps with over 50 million cumulative downloads as a solo developer means checking Play Console every day. Before v2.0.0, I was seeing 1–2 crashes per day. After release, it jumped to 5–7, and Crash-free users dropped below 99.5% before I caught it.

Antigravity's First Suggestion Missed the Real Problem

I pasted the stack trace directly into Antigravity and asked what was causing it. The response suggested adding synchronized access:

// Antigravity's first suggestion — this doesn't actually fix it
fun updateList(newItems: List<WallpaperItem>) {
    synchronized(this) {
        items.clear()
        items.addAll(newItems)
        notifyDataSetChanged()
    }
}

The reasoning was sound in principle — synchronization prevents race conditions. But it missed something fundamental: items was a reference to an external MutableList, not an internal copy.

The Root Cause: Holding a Reference to an External List

During the v2.0.0 refactor, I had changed the WallpaperAdapter constructor to accept a mutable list directly:

// Problematic code (v2.0.0)
class WallpaperAdapter(
    private val items: MutableList<WallpaperItem>  // external reference kept as-is
) : RecyclerView.Adapter<WallpaperAdapter.ViewHolder>() {
 
    override fun getItemCount() = items.size
    // ...
}

On the Activity side, the adapter was wired up like this:

val wallpapers = viewModel.getWallpapers().toMutableList()
val adapter = WallpaperAdapter(wallpapers)
recyclerView.adapter = adapter
 
// Later, in another part of the code:
wallpapers.removeAt(0)  // ← this triggers the crash

Because the adapter holds a reference to the same list object, when wallpapers.removeAt(0) is called outside the adapter, the list size changes without the adapter knowing. RecyclerView expects 13 items; it finds 12. Crash.

The problem wasn't missing synchronization — it was that the adapter had no ownership over its own data.

The Fix: Defensive Copy

The fix is a single line in the constructor:

// Fixed code (v2.1.0)
class WallpaperAdapter(
    items: List<WallpaperItem>  // changed to List, copy internally
) : RecyclerView.Adapter<WallpaperAdapter.ViewHolder>() {
 
    private val items: MutableList<WallpaperItem> = items.toMutableList()  // defensive copy
 
    override fun getItemCount() = items.size
 
    fun submitList(newItems: List<WallpaperItem>) {
        items.clear()
        items.addAll(newItems.toMutableList())  // copy on update too
        notifyDataSetChanged()
    }
    // ...
}

By calling items.toMutableList(), the adapter owns its own internal copy. No matter what happens to the list passed in from the outside, the adapter's state is unaffected.

After shipping this as v2.1.0 — staged rollout at 5% → 25% → 50% → 100%, monitoring Crash-free users at each step — the crashes dropped to near zero. Crash-free users held above 99.7%.

Why Antigravity Missed It — and How I Asked Better Questions

The first response was off because I only gave Antigravity a stack trace and asked "what's causing this?" Antigravity inferred thread contention from the trace and suggested synchronization — a reasonable guess without full context.

When I switched the approach:

Paste the full adapter constructor + update method code
→ Ask: "Where in this code could IndexOutOfBoundsException occur?"

Antigravity immediately identified it: "You're storing a reference to an external list. If something outside the adapter modifies that list without calling notify, the adapter's state diverges."

After more than 12 years of solo app development, one consistent lesson with AI coding tools is this: asking "where could this break?" with the actual code in view gets you further than asking "what does this error mean?" with only a stack trace. Structure over symptoms.

A Checklist for RecyclerView Crashes

If you're seeing inconsistency-detected crashes, check these:

  • Is the adapter constructor storing a direct reference to a MutableList passed in from outside?
  • Is the list being modified externally between notify calls?
  • Is any list modification happening on a background thread without the adapter's involvement?
  • If using DiffUtil, are the diff calculation and UI update steps properly separated?

For new projects, ListAdapter with DiffUtil is worth using from the start — it handles internal list management, making the defensive copy pattern unnecessary:

// More robust approach: ListAdapter + DiffUtil
class WallpaperAdapter : ListAdapter<WallpaperItem, WallpaperAdapter.ViewHolder>(
    object : DiffUtil.ItemCallback<WallpaperItem>() {
        override fun areItemsTheSame(oldItem: WallpaperItem, newItem: WallpaperItem) =
            oldItem.id == newItem.id
        override fun areContentsTheSame(oldItem: WallpaperItem, newItem: WallpaperItem) =
            oldItem == newItem
    }
) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // ...
    }
 
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position))  // ListAdapter manages the internal list safely
    }
}

Whether you go with a defensive copy or migrate to ListAdapter, the underlying principle is the same: the adapter should own its own state. Once I internalized that, this class of crash stopped appearing entirely in my apps.

If you're working through a similar issue, I hope this record saves you some time.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-07-18
After Compose-First: Choosing Which View Screens to Migrate, Ranked by Churn Instead of Count
Google has declared Android development Compose-first. Here is how I rank View-based screens for migration using git history rather than screen counts, with the scoring script I actually ran and the three places partial migration quietly duplicates state.
App Dev2026-07-03
Catching Download Size Regressions Before Submission Day — A Weekly Agent Gate for AAB/IPA Size Budgets
Mediation SDKs and bundled assets quietly inflate download size. A design for size ledgers, budget gates, and agent-driven delta attribution using bundletool and App Thinning reports.
App Dev2026-06-30
The App Open Ad Antigravity Wrote for Me Fires Every Time I Return From My Own Paywall or Rewarded Video
Ask Antigravity to add an App Open ad and it shows one the instant you return from your own rewarded video or the Google Play purchase sheet — which also brushes against AdMob policy. Here is a foreground arbiter that records why the app came back, with working Kotlin and a verification matrix.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →