After releasing Beautiful HD Wallpapers for Android v2.0.0, over 50 users hit IndexOutOfBoundsException crashes within 28 days. Firebase Crashlytics kept pointing to the same place: onBindViewHolder in the RecyclerView adapter.
When I first asked Antigravity to "fix this RecyclerView crash," it came back with a try-catch that swallowed the exception. The red squiggle disappeared. The crashes kept arriving the next day.
Getting Antigravity to correctly fix concurrency bugs requires telling it why the crash happens — not just that it crashes. This article walks through the approach that eliminated the crash in v2.1.0.
The Pattern Antigravity Tends to Suggest First
Ask Antigravity to fix an IndexOutOfBoundsException in RecyclerView, and it will often propose something like this:
// What Antigravity tends to suggest first (not a real fix)
override fun onBindViewHolder(holder: WallpaperViewHolder, position: Int) {
try {
holder.bind(items[position])
} catch (e: IndexOutOfBoundsException) {
// Suppress the crash
e.printStackTrace()
}
}Or it might add an if (position < items.size) guard and call it done. These reduce crash frequency but leave the root cause intact.
The issue is not that position is an invalid value. It's that getItemCount() and onBindViewHolder() disagree on the list size because a background thread modified it in between. Antigravity sees a code snapshot — it cannot infer cross-thread state changes unless you tell it explicitly.
The Root Cause
In most cases, this crash happens because the adapter holds a direct reference to an external list.
// Problematic code
class WallpaperAdapter(
private val items: List<Wallpaper> // direct reference to caller's list
) : RecyclerView.Adapter<WallpaperViewHolder>() {
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: WallpaperViewHolder, position: Int) {
holder.bind(items[position]) // crash happens here
}
}Here's the timing: the UI thread calls getItemCount() and gets 100. Then a background thread removes 2 items. Then onBindViewHolder(position=99) is called — but the list now has 98 items. IndexOutOfBoundsException.
In Beautiful HD Wallpapers, background wallpaper filtering competed with the UI thread's rendering during category selection. Code written back in 2014 — when I started building apps full-time — didn't give thread safety the attention it deserves today.
How to Prompt Antigravity for the Right Fix
Give Antigravity the root cause and the fix direction. This changes the output entirely.
Please fix the IndexOutOfBoundsException in WallpaperAdapter.
Root cause: The adapter holds a direct reference to an external list.
When a background thread updates that list, the size seen by getItemCount()
at time T differs from the size seen by onBindViewHolder() at time T+1,
causing IndexOutOfBoundsException.
Fix approach:
- Create a defensive copy in submitList() using ArrayList(source)
- Never assign the external list reference directly to the adapter's field
- Add a DiffUtil-based diff for animated updates
- Do NOT wrap in try-catch — fix the ownership, not the symptom
With this prompt, Antigravity suggests a proper ownership-based design instead of exception suppression.
The Defensive Copy Implementation
Here's the fixed adapter from v2.1.0, based on Antigravity's suggestion after receiving the detailed prompt:
// Fixed: defensive copy pattern
class WallpaperAdapter : RecyclerView.Adapter<WallpaperViewHolder>() {
// Internal copy — no external list references
private val items = mutableListOf<Wallpaper>()
fun submitList(newItems: List<Wallpaper>) {
// Defensive copy: sever the reference to the caller's list
val copy = ArrayList(newItems)
val diff = DiffUtil.calculateDiff(
WallpaperDiffCallback(items, copy)
)
items.clear()
items.addAll(copy)
diff.dispatchUpdatesTo(this)
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: WallpaperViewHolder, position: Int) {
// Position guard as a last line of defense
if (position >= items.size) return
holder.bind(items[position])
}
}After shipping this in v2.1.0, Crash-free users improved from 98.2% to 99.8%. The 50-user crash accumulation over 28 days stopped. The relief was quiet but real — this is the kind of improvement that makes over 50 million downloads feel like a responsibility worth taking seriously.
Encoding the Rule in AGENTS.md
A more sustainable approach than re-explaining in every prompt is to put the convention in AGENTS.md. Once it's there, Antigravity applies the defensive copy pattern automatically whenever it touches RecyclerView code.
# AGENTS.md (Android project)
## RecyclerView and list handling rules
- Adapters must NOT hold direct references to external lists
- Any submitList() method must create a defensive copy: ArrayList(source)
- Correct: `val copy = ArrayList(newItems)` → `items.addAll(copy)`
- Incorrect: `this.items = newItems` (direct reference assignment)
- Always add a position guard in onBindViewHolder() when the list can change concurrently:
`if (position >= items.size) return`
- Use DiffUtil for list updates; avoid notifyDataSetChanged() as a blanket updateAfter 12 years of solo app development — across four iOS apps and two Android apps with over 50 million combined downloads — I've found that building up AGENTS.md with hard-won conventions is the most durable way to keep an AI coding partner aligned. My grandfather was a temple carpenter who would say that arranging your tools properly is how the work begins. AGENTS.md is exactly that for me now.
Related Patterns to Watch Out For
The same shared-list-reference problem appears outside RecyclerView. Before asking Antigravity for a fix, check these areas too:
ViewPager2's FragmentStateAdapter: If the adapter's List<Fragment> is modified externally, the same crash occurs. The defensive copy pattern applies identically.
Legacy ArrayAdapter and spinners: Confirm that the list is owned by the adapter before calling notifyDataSetChanged(). Older codebases often have this issue lingering.
Bulk updates without DiffUtil: notifyDataSetChanged() skips diff calculation and causes visual flicker on large datasets. Introducing defensive copies is a good moment to also migrate to DiffUtil.
Run grep -r "private val.*: List<" app/src/ to find adapters in your project that still hold external list references. You may find more candidates than expected.