◉CLAUDE LABEN
●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/Claude Code
⟐ Claude Code/2026-05-16中級

Claude Code でAndroid の RecyclerView クラッシュを28日間で根治した記録

Beautiful HD Wallpapersのv2.0.0リリース後、RecyclerViewのIndexOutOfBoundsExceptionが28日で50ユーザー以上に発生。Claude Codeとの対話で防御的コピーによる根治法を発見した実録。

android9kotlin3recyclerviewcrashdebugging5claude-code132

v2.0.0 をリリースした28日後、Firebase Crashlytics のダッシュボードに赤いグラフが現れました。

IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder — 50ユーザー以上が同じクラッシュで落ちています。Beautiful HD Wallpapers のリニューアルに自信を持っていた分、このエラーの根深さが際立ちました。

notifyDataSetChanged() を入れてみたり、DiffUtil に切り替えてみたりしました。表面的な対処を繰り返しても、Crashlytics のグラフは下がりませんでした。最終的に Claude Code との対話でたどり着いたのが「防御的コピー」です。診断から修正までのプロセスを、同じ問題で詰まっている方のために書き残しておきます。

RecyclerView の IndexOutOfBoundsException はなぜ厄介か

RecyclerView のこのエラーは、スタックトレースだけ見ると原因がわかりません。

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder
at androidx.recyclerview.widget.RecyclerView$Recycler.validateViewHolderForOffscreenChange

RecyclerView はデータソース(List<Wallpaper>)が外部から変更されていないという前提で動きます。アダプターへのデータ渡しと、バックグラウンドでのリスト更新が競合すると、RecyclerView 内部の「想定サイズ」と「実際のサイズ」がずれてクラッシュします。

問題は、このズレが常に再現するわけではないことです。マルチコア環境での微妙なタイミングで発生するため、ローカルでの再現が難しく、Crashlytics で見る頻度に対してデバッグが追いつかない状況が続きました。個人開発を2014年から続けてきた経験上、このタイプのバグは「ちゃんと書いた気がする」コードに潜んでいることが多く、最も時間がかかるカテゴリのひとつです。

Claude Code に診断を依頼したときの対話

壁紙アプリ Beautiful HD Wallpapers のコアとなる WallpaperListAdapter と、データを流す WallpaperRepository のコードを貼り付けて、Claude Code に診断を依頼しました。

わたし: RecyclerViewのIndexOutOfBoundsExceptionが本番で多発しています。
        スタックトレースと関連クラスを添付します。
        どのコードパスで発生しているか、診断してください。

Claude Code: WallpaperRepository.getWallpapers() が返す List<Wallpaper> を
            WallpaperListAdapter に直接渡している箇所を確認できます。
            Repository 内で同じリストオブジェクトへの参照が外部に渡されているため、
            バックグラウンドでのデータ更新時にアダプターが参照しているリストが
            変更されています。これが Inconsistency の根本原因です。

「同じリストオブジェクトへの参照」という一言で、問題の構造が明確になりました。

自分でデバッグしているときは「どこを直すか」を探してしまいます。Claude Code への相談を「どこが問題の構造なのか」という問いに変えたとき、_wallpaperList への参照漏れというアーキテクチャレベルの問題が浮かび上がりました。個人開発者にとって、コードレビューを依頼できる同僚がいないぶん、この種の診断対話は特に有用だと感じています。

私のコードでは、Repository が内部で保持する _wallpaperList をそのままアダプターに渡していました。ページング処理でバックグラウンドスレッドが _wallpaperList に追加操作をする間、RecyclerView の描画スレッドは「変更前のサイズ」で動いている — それがクラッシュの正体でした。

防御的コピーによる修正

Claude Code が提案した修正はシンプルでした。Repository がリストを返すとき、内部リストそのものではなく、その瞬間のスナップショットを返すようにするというものです。

修正前のコードでは、Repository が直接 _wallpaperList への参照を返していました。外部のアダプターはその参照を受け取るため、Repository 側でリストが変更されると、アダプターが持っているリストも意図せず変わってしまいます。

// 修正前: 内部リストへの参照がそのまま渡っていた
fun getWallpapers(): List<Wallpaper> {
    return _wallpaperList
}

修正後は toList() を使って防御的コピーを返します。

// 修正後: イミュータブルなスナップショットを返す
fun getWallpapers(): List<Wallpaper> {
    return _wallpaperList.toList()
}

toList() は Kotlin の標準関数で、元のリストの内容をコピーした新しいイミュータブルリストを作ります。アダプターが受け取るリストは「渡した瞬間の状態」で固定されるため、その後 Repository 側でいくら変更しても RecyclerView の内部状態とズレが起きません。

あわせて、アダプター側でもデータ更新時に明示的にコピーを取るよう変更しました。

class WallpaperListAdapter : RecyclerView.Adapter<WallpaperViewHolder>() {
    private var wallpapers: List<Wallpaper> = emptyList()
 
    fun submitList(newList: List<Wallpaper>) {
        // 防御的コピー+DiffUtilで差分更新
        val defensiveCopy = newList.toList()
        val diffResult = DiffUtil.calculateDiff(
            WallpaperDiffCallback(wallpapers, defensiveCopy)
        )
        wallpapers = defensiveCopy
        diffResult.dispatchUpdatesTo(this)
    }
 
    override fun getItemCount(): Int = wallpapers.size
}

DiffUtil と組み合わせることで、差分更新のアニメーションも維持しつつ、スレッドセーフな更新ができるようになりました。

v2.1.0 リリース後の結果

修正を含む v2.1.0 を段階公開(5% → 25% → 50% → 100%)でリリースしました。Crashlytics のグラフで確認できた変化は明確でした。IndexOutOfBoundsException によるクラッシュはゼロになり、Crash-free users は 99.8% で安定を維持しています。段階公開中にクラッシュ増加が見られなかったため、安心して 100% ロールアウトの判断ができました。

28日間悩んでいた問題が、コードの変更としては数行で解決しました。修正の規模が問題の深刻さに比例しないのが、こういった並行処理バグの特徴だと思います。

この問題が起きやすい他のパターン

防御的コピーが必要になる場面は他にもあります。StateFlow や LiveData を使っている場合でも、可変リストを直接 .value に代入すると同じ問題が起きます。

// ❌ よくある危険なパターン(StateFlow でも同じ問題が潜む)
class CategoryRepository {
    private val _categories = mutableListOf<Category>()
    private val _categoriesFlow = MutableStateFlow<List<Category>>(emptyList())
 
    fun update(newCategories: MutableList<Category>) {
        _categoriesFlow.value = newCategories  // 可変リストへの参照がそのまま流れる
    }
}
 
// ✅ 安全なパターン
class CategoryRepository {
    private val _categories = mutableListOf<Category>()
 
    // 防御的コピーを返す
    fun getCategories(): List<Category> = _categories.toList()
 
    // StateFlow の場合もコピーしてから代入
    private val _categoriesFlow = MutableStateFlow<List<Category>>(emptyList())
    val categoriesFlow: StateFlow<List<Category>> = _categoriesFlow.asStateFlow()
 
    fun update(newCategories: List<Category>) {
        _categoriesFlow.value = newCategories.toList()
    }
}

私が今実践しているルールは「リストをコンポーネントや別スレッドの境界をまたいで渡す前に、必ず .toList() を呼ぶ」です。習慣化してからは、同様のクラッシュが再発していません。RecyclerView 以外でも、ViewPager2 やカスタム View でデータリストを保持する場合は同様の注意が必要です。

次に書く予定のこと

Beautiful HD Wallpapers の Android 版は引き続き更新を続けています。次に書こうと思っているのは、Glide 5.0.5 と AGP 9.x の組み合わせで発生した Java 8 の互換性クラッシュです。こちらは coreLibraryDesugaringEnabled true の1行追加で解決しましたが、その原因にたどり着くまでの経緯が今回と同じくらい面白かったので、また整理して書きます。

もし同じ IndexOutOfBoundsException で詰まっている方がいれば、まず Repository からアダプターへの「参照の渡し方」を確認してみてください。修正は小さいはずです。デバッグの時間より、実装の楽しさに集中できる時間が増えることを願っています。ViewPager2 やカスタム View でも同様の参照漏れは起こりえます。Android 開発で並行処理由来のクラッシュに直面したとき、「防御的コピー」という選択肢を思い出していただければ幸いです。

Claude Code を使ったデバッグのアプローチについて

今回改めて感じたのは、「コードを貼って質問する」だけでなく「診断を依頼する」というアプローチの有効性です。自分でデバッグしているときは「どこを直すか」を探してしまいがちです。Claude Code への問いを「どこが問題の構造なのか」に変えたとき、表面的な修正候補ではなく、アーキテクチャレベルの問題が見えてきました。

AdMob 広告を収益源に個人開発を2014年ごろから続けています。Beautiful HD Wallpapers をはじめとした壁紙アプリ群は累計5,000万DLを超えており、一人でバグ対応から収益管理まで担っています。こういった並行処理由来のバグに限らず、「自分では見えていなかった問題の構造」を発見するために他者の視点が必要になる場面は何度もありました。10年前なら信頼できる同僚にレビューを頼む場面です。今は Claude Code に「診断してください」と投げることで、同様の視点を得られます。

問いの立て方さえ変えれば、Claude Code はコードレビュアーとして機能します。「このコードを直して」ではなく「このコードの問題の構造を教えて」と問いかけることで、根本原因にたどり着く速度が変わりました。私自身、この方法に切り替えてから、同種の問題に費やす時間が大幅に減ったと感じています。

問いの立て方さえ変えれば、Claude Code はコードレビュアーとして機能します。「このコードを直して」ではなく「このコードの問題の構造を教えて」と問いかけることで、根本原因にたどり着く速度が大きく変わりました。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⟐ Claude Code2026-06-12
Android の戻るボタン広告ゲートを入れ子から並列独立構造へ — Claude Code と進めた終了フロー再設計の記録
戻るボタンの広告が「出ない」と「出すぎる」を同時に起こした原因は、入れ子分岐に埋め込まれた暗黙の優先度でした。壁紙アプリ v2.1.0 で広告ゲートを並列独立構造へ作り直した過程を、Kotlin実装・Claude Codeへの依頼内容・テストまで含めて記録します。
⟐ Claude Code2026-05-27
壁紙アプリ 2 本の Crashlytics を Claude Code で 11 日間掘り続けた実装ログ
壁紙アプリ 2 本(綺麗な壁紙 v2.0.0 / 浮世絵壁紙 v1.7.0)でリリース後に Crashlytics と Play Console から噴き出した 30 件超のバグを、Claude Code で 11 日間掘り続けて v2.1.1 / v1.8.1 まで仕上げた工程を、スタックトレース → 原因仮説 → 修正コミットの順で公開します。
⟐ Claude Code2026-04-24
Claude Code Hooks の本番デバッグ術 — ログが足りない時、どこから切り分けるか
ローカルでは動くのに本番で沈黙するClaude Code Hooksの切り分け方を体系化しました。まず疑うべきは出力先という視点、ログ不足を補う3層の可観測性、最小再現環境を60秒で作るテクニック、JSON入出力とexit codeのプロトコル把握、マルチユーザー環境の権限診断まで扱います。