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

When Antigravity Fixes a RecyclerView Crash but Breaks Something Else — The Defensive Copy Solution

If RecyclerView IndexOutOfBoundsExceptions keep coming back after Antigravity's fix, shared list reference is likely the cause. Here's how to prompt Antigravity for the right fix and encode the pattern in AGENTS.md.

android28RecyclerViewcrash fixapp-dev49troubleshooting108defensive copy

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 update

After 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.

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-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-21
The Back Button Showed an Interstitial Sometimes, Not Others — Rewriting Nested ifs Into a List of Independent Guards
Interstitial display on back press was unstable because nested if statements hid the priority between conditions. Here is how I split it into reason-returning guards and generated tests from a decision table.
App Dev2026-05-17
Android Theme Switch White Screen: Replacing Activity.recreate() with AppRestarter
A real fix for the white screen that appears when switching themes on Android. Covers why Activity.recreate() causes the issue, how AppRestarter.safeRestart() eliminates it, and a ModalGate pattern to prevent dialog conflicts after restart.
📚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 →