Skip to content

Crash: IllegalArgumentException in LazyList SubcomposeLayout recurs on Android 16 (Samsung Galaxy A15) (#4427)#4434

Merged
johnnyluo merged 5 commits into
mainfrom
agent/4427-crash--illegalargumentexception-in-lazyl
May 8, 2026
Merged

Crash: IllegalArgumentException in LazyList SubcomposeLayout recurs on Android 16 (Samsung Galaxy A15) (#4427)#4434
johnnyluo merged 5 commits into
mainfrom
agent/4427-crash--illegalargumentexception-in-lazyl

Conversation

@Vaulty-bot
Copy link
Copy Markdown
Collaborator

@Vaulty-bot Vaulty-bot commented May 7, 2026

Fixes #4427

Changes

  • TokenSelectionList.kt: Changed itemsIndexed key in the grouped grid from index-based "$groupIndex-$itemIndex" to content-based "${title ?: "group$groupIndex"}-${item.hashCode()}" to avoid duplicate keys across groups.
  • TransactionHistoryViewModel.kt: Added dateKey: String field to TransactionHistoryGroupUiModel and populated it with date.toString() when constructing groups.
  • TransactionHistoryScreen.kt: Replaced the stickyHeader key from "header_${group.dateSuffix}" to "header_${group.dateKey}" (stable across UiText changes), and wrapped groups in remember(groups) { groups } snapshot before iteration; updated all three preview composables to pass the new dateKey.
  • ImportFileScreen.kt: Simplified ZipOutput's itemsIndexed key from "$index:${zipOutput.name}" to just zipOutput.name, dropping the index from the key.

Checklist

  • Lint clean
  • Build verified (S1)
  • Self-reviewed against requirements (S3)
  • No secrets committed
  • Conventional commit messages

Summary by CodeRabbit

  • Refactor
    • Improved stability of token lists and transaction history rendering to reduce visual shifting during updates.
    • Sticky transaction date headers now remain consistent across updates for clearer timeline grouping.
    • Import file list items retain consistent identity to prevent unexpected row changes during UI updates.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Warning

Rate limit exceeded

@Vaulty-bot has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 57 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 58bc0792-93c1-40df-a1fe-23e26ac539fe

📥 Commits

Reviewing files that changed from the base of the PR and between 88a5007 and e667284.

📒 Files selected for processing (2)
  • app/src/main/java/com/vultisig/wallet/ui/models/ImportFileViewModel.kt
  • data/src/main/kotlin/com/vultisig/wallet/data/common/FileHelper.kt
📝 Walkthrough

Walkthrough

Adds a stable dateKey to transaction group UI models and replaces index-based keys with content-derived keys in Lazy lists/grids; previews updated and transaction groups cached via remember before rendering.

Changes

Compose Key Stability

Layer / File(s) Summary
Data Shape
app/src/main/java/com/vultisig/wallet/ui/models/TransactionHistoryViewModel.kt
TransactionHistoryGroupUiModel adds dateKey: String.
ViewModel Population
app/src/main/java/com/vultisig/wallet/ui/models/TransactionHistoryViewModel.kt
groupByDate sets dateKey from LocalDate.toString() for each group.
Token List Keying
app/src/main/java/com/vultisig/wallet/ui/components/v2/tokenitem/TokenSelectionList.kt
LazyVerticalGrid key now combines group title (or group-index fallback) + item hashCode + itemIndex instead of using indices alone.
Import File Keying
app/src/main/java/com/vultisig/wallet/ui/screens/ImportFileScreen.kt
ZipOutput LazyColumn.itemsIndexed key simplified to zipOutput.name.
Transaction History Keying
app/src/main/java/com/vultisig/wallet/ui/screens/transaction/TransactionHistoryScreen.kt
TransactionGroupedList uses remember(groups) snapshot and sticky header keys based on group.dateKey ("header_${group.dateKey}") instead of dateSuffix.
Preview Updates
app/src/main/java/com/vultisig/wallet/ui/screens/transaction/TransactionHistoryScreen.kt
Preview data for "Today" and "Yesterday" groups updated to include dateKey.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

"I hopped through keys and dates today,
Replacing indices gone astray.
dateKey sings; the lists align—
No more crashes, layouts fine.
🐇✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective: fixing a crash caused by LazyList/SubcomposeLayout issues on Android 16, which aligns with all file changes targeting LazyList key stability.
Linked Issues check ✅ Passed All code changes directly address the linked issue #4427 requirements: audit and fix unstable LazyList item keys by implementing content-based keys (TokenSelectionList, ImportFileScreen) and stable group identifiers (TransactionHistoryViewModel/Screen).
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing LazyList key stability issues identified in #4427; preview updates and remember wrapping are necessary supporting changes to propagate the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/4427-crash--illegalargumentexception-in-lazyl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/vultisig/wallet/ui/components/v2/tokenitem/TokenSelectionList.kt (1)

159-169: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

hashCode() can produce duplicate keys within a group — same crash as the one being fixed.

GridTokenUiModel is a data class-based sealed type; its hashCode() derives from field values and is not guaranteed unique. If any two items in the same group collide on hash (statistically unlikely but non-zero for large token lists), Compose sees duplicate keys and throws the identical IllegalArgumentException this PR targets.

The cross-group prefix "${title ?: "group$groupIndex"}" prevents collisions between groups but gives no protection within a group.

Minimum fix — add itemIndex as a uniqueness tiebreaker while keeping content-based sorting:

🔧 Proposed fix
-                            key = { _, item ->
-                                "${title ?: "group$groupIndex"}-${item.hashCode()}"
-                            },
+                            key = { itemIndex, item ->
+                                "${title ?: "group$groupIndex"}-${item.hashCode()}_$itemIndex"
+                            },

Ideal long-term fix: add a keyExtractor: (GridTokenUiModel<T>) -> Any parameter so callers can supply a domain-level stable ID (e.g. coin.id).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/vultisig/wallet/ui/components/v2/tokenitem/TokenSelectionList.kt`
around lines 159 - 169, The current itemsIndexed key uses item.hashCode() which
can collide for different GridTokenUiModel instances and cause duplicate-key
crashes; update the key lambda passed to itemsIndexed (the key = { index, item
-> ... } parameter) to include the item index (or other stable per-item
tiebreaker) together with the content-derived value so keys are unique within a
group while preserving content-based identity (e.g., combine the group
prefix/title, item.hashCode(), and the index), and consider adding a long-term
improvement by introducing a keyExtractor parameter so callers can supply a
stable domain ID for GridTokenUiModel.
🧹 Nitpick comments (1)
app/src/main/java/com/vultisig/wallet/ui/screens/transaction/TransactionHistoryScreen.kt (1)

217-223: 💤 Low value

remember(groups) { groups } is a no-op — consider removing it.

remember(key) { block } returns block() when key changes and the cached value otherwise. In both branches snapshot holds exactly the same reference as groups. It does not create a defensive snapshot of the list during SubcomposeLayout measurement, and it cannot prevent a recomposition triggered by the groups StateFlow emission. The actual crash fix is the stable dateKey on the sticky header; this wrapper only adds confusion.

♻️ Proposed simplification
-    val snapshot = remember(groups) { groups }
     LazyColumn(
         modifier = modifier,
         contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
         verticalArrangement = Arrangement.spacedBy(8.dp),
     ) {
-        snapshot.forEach { group ->
+        groups.forEach { group ->
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/vultisig/wallet/ui/screens/transaction/TransactionHistoryScreen.kt`
around lines 217 - 223, Remove the redundant remember wrapper: delete the line
declaring val snapshot = remember(groups) { groups } and update the LazyColumn
body to iterate directly over groups (replace snapshot.forEach { ... } with
groups.forEach { ... }); keep the rest of the LazyColumn (modifier,
contentPadding, verticalArrangement, headers) unchanged so the code uses the
original groups list directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@app/src/main/java/com/vultisig/wallet/ui/components/v2/tokenitem/TokenSelectionList.kt`:
- Around line 159-169: The current itemsIndexed key uses item.hashCode() which
can collide for different GridTokenUiModel instances and cause duplicate-key
crashes; update the key lambda passed to itemsIndexed (the key = { index, item
-> ... } parameter) to include the item index (or other stable per-item
tiebreaker) together with the content-derived value so keys are unique within a
group while preserving content-based identity (e.g., combine the group
prefix/title, item.hashCode(), and the index), and consider adding a long-term
improvement by introducing a keyExtractor parameter so callers can supply a
stable domain ID for GridTokenUiModel.

---

Nitpick comments:
In
`@app/src/main/java/com/vultisig/wallet/ui/screens/transaction/TransactionHistoryScreen.kt`:
- Around line 217-223: Remove the redundant remember wrapper: delete the line
declaring val snapshot = remember(groups) { groups } and update the LazyColumn
body to iterate directly over groups (replace snapshot.forEach { ... } with
groups.forEach { ... }); keep the rest of the LazyColumn (modifier,
contentPadding, verticalArrangement, headers) unchanged so the code uses the
original groups list directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6994bca6-4ac3-4490-85fb-10dfec7ba807

📥 Commits

Reviewing files that changed from the base of the PR and between 1cadf2d and cd31095.

📒 Files selected for processing (4)
  • app/src/main/java/com/vultisig/wallet/ui/components/v2/tokenitem/TokenSelectionList.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/TransactionHistoryViewModel.kt
  • app/src/main/java/com/vultisig/wallet/ui/screens/ImportFileScreen.kt
  • app/src/main/java/com/vultisig/wallet/ui/screens/transaction/TransactionHistoryScreen.kt

@Vaulty-bot
Copy link
Copy Markdown
Collaborator Author

Picked up CodeRabbit feedback (actionable issues only, skipping nitpicks). Working on it.

Add itemIndex as tiebreaker in TokenSelectionList grid key to guard
against hashCode collisions within a group.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Vaulty-bot
Copy link
Copy Markdown
Collaborator Author

Pushed coderabbit fixes in 88a5007. Ready for re-review.

Addressed

  • TokenSelectionList.kt L159-169 (⚠️ Major — outside-diff): Added itemIndex as tiebreaker in the itemsIndexed key ("${title ?: "group$groupIndex"}-${item.hashCode()}_$itemIndex") to guard against hashCode() collisions within a group, which would otherwise reproduce the exact crash this PR targets.

Declined

  • TransactionHistoryScreen.kt L217-223 (🧹 Nitpick — outside-diff): Removing the remember(groups) { groups } snapshot wrapper. Skipping per default policy of not addressing nitpicks unless explicitly requested.

Note: both review findings were posted as "outside diff range" comments embedded in the review body (no inline comment IDs were created), so per-thread inline replies aren't possible — this summary covers both.

@aminsato
Copy link
Copy Markdown
Collaborator

aminsato commented May 7, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@app/src/main/java/com/vultisig/wallet/ui/components/v2/tokenitem/TokenSelectionList.kt`:
- Around line 161-163: The key generation for items in TokenSelectionList
currently omits groupIndex when title is non-null, causing potential key
collisions; update the key lambda (the key = { itemIndex, item -> ... } in
TokenSelectionList) to always include groupIndex as a prefix (e.g., start the
key with groupIndex followed by the title or a default) so each group's items
are globally unique across groups even when titles repeat.

In `@app/src/main/java/com/vultisig/wallet/ui/screens/ImportFileScreen.kt`:
- Line 206: The LazyColumn call uses itemsIndexed(zipOutputs, key = { _,
zipOutput -> zipOutput.name }) which can throw if names are duplicated; update
the key so it is unique (for example use the index as a tiebreaker or the index
alone) — change the key lambda in the itemsIndexed call to include the index
(e.g. key = { index, zipOutput -> "${index}_${zipOutput.name}" } or key = {
index, _ -> index }) to guarantee uniqueness for itemsIndexed and avoid
IllegalArgumentException.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7b5747db-125a-4529-93cc-371361f1bdf0

📥 Commits

Reviewing files that changed from the base of the PR and between 1cadf2d and 88a5007.

📒 Files selected for processing (4)
  • app/src/main/java/com/vultisig/wallet/ui/components/v2/tokenitem/TokenSelectionList.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/TransactionHistoryViewModel.kt
  • app/src/main/java/com/vultisig/wallet/ui/screens/ImportFileScreen.kt
  • app/src/main/java/com/vultisig/wallet/ui/screens/transaction/TransactionHistoryScreen.kt

@Vaulty-bot
Copy link
Copy Markdown
Collaborator Author

Picked up CodeRabbit feedback (actionable issues only, skipping nitpicks). Working on it.

Make LazyList keys collision-proof:
- TokenSelectionList: always prefix item key with groupIndex so duplicate
  group titles cannot produce identical keys.
- ImportFileScreen: include the index as a tiebreaker for zip entry names
  in case a ZIP contains duplicate entry names.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Vaulty-bot
Copy link
Copy Markdown
Collaborator Author

Addressed CodeRabbit review feedback in 3017147.

Comments fixed (2/2):

  • TokenSelectionList.kt:163 — Always prefix the LazyVerticalGrid item key with groupIndex so two groups with the same title cannot generate colliding keys.
  • ImportFileScreen.kt:206 — Use the index as a tiebreaker ("${index}_${zipOutput.name}") so duplicate ZIP entry names won't crash Compose.

Comments declined: none.

@Vaulty-bot
Copy link
Copy Markdown
Collaborator Author

Picked up CodeRabbit feedback (actionable issues only, skipping nitpicks). Working on it.

@Vaulty-bot
Copy link
Copy Markdown
Collaborator Author

Only nitpick feedback remaining — no action taken.

Latest CodeRabbit review (run b1bcee63) reports Inline Comments: [] (no actionable items). The 2 prior actionable findings were already fixed in 3017147 and confirmed by CodeRabbit:

  • TokenSelectionList.kt:163 — groupIndex always prefixed in the LazyVerticalGrid key
  • ImportFileScreen.kt:206 — index used as tiebreaker in itemsIndexed key

The only remaining flag is the global Docstring Coverage pre-merge warning (0%). The diff adds a single dateKey: String property to TransactionHistoryGroupUiModel; its sibling properties are also undocumented, so adding KDoc to it alone would be inconsistent drive-by churn outside the scope of this bug fix.

@Vaulty-bot
Copy link
Copy Markdown
Collaborator Author

CI failure detected. Working on a fix.

Comment thread data/src/main/kotlin/com/vultisig/wallet/data/common/FileHelper.kt
Comment thread data/src/main/kotlin/com/vultisig/wallet/data/common/FileHelper.kt
Comment thread app/src/main/java/com/vultisig/wallet/ui/screens/ImportFileScreen.kt Outdated
- TokenSelectionList: revert to "$groupIndex-$itemIndex"; the previous
  key was already collision-free since both indices come from positional
  iterators, and the hashCode/title segments added no uniqueness benefit.
- FileHelper.fileContent: rethrow CancellationException before the
  generic Exception catch so coroutine cancellation propagates, matching
  VaultAccountsViewModel/DepositFormViewModel.
- ImportFileViewModel.saveFileToAppDir: when fileContent returns null,
  surface the import_file_not_supported error instead of silently
  storing null and bailing out of parseFileContent.
- ImportFileScreen.ZipOutput: drop the index from the LazyColumn key
  so it is just zipOutput.name; keeping the index left it identical to
  the previous key while shifting indices on filtered removals.
@Vaulty-bot
Copy link
Copy Markdown
Collaborator Author

Addressed @rkokhatskyi's review feedback in e667284.

Fixed (4/4 inline comments)

  • TokenSelectionList.kt (id=3204597280): reverted to "$groupIndex-$itemIndex". Confirmed the original key was already collision-free since both indices come from positional iterators, and the previous attempted variant ended in _$itemIndex so it behaved identically for reorders.
  • FileHelper.kt:150 (id=3204597284): added catch (e: CancellationException) { throw e } before the generic Exception catch so coroutine cancellation propagates, matching VaultAccountsViewModel.kt:233 and DepositFormViewModel.kt:1787.
  • FileHelper.kt:152 (id=3204597288): ImportFileViewModel.saveFileToAppDir now surfaces the import_file_not_supported error and clears the import state when fileContent returns null, mirroring the fetchFileName failure path. The user gets visible feedback instead of a silently dead screen.
  • ImportFileScreen.kt:206 (id=3204597291): dropped the index from the key so it is just zipOutput.name. The previous index-prefixed variant left the key effectively the same as before and shifted on filtered removals.

Declined: none.

Verification: ./gradlew ktfmtCheck passes. Local testDebugUnitTest cannot run in this sandbox (WalletCore Maven 401 — missing GitHub credentials); CI will exercise full build/lint/tests.

Copy link
Copy Markdown
Contributor

@johnnyluo johnnyluo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@johnnyluo johnnyluo enabled auto-merge May 8, 2026 00:07
@johnnyluo johnnyluo added this pull request to the merge queue May 8, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 8, 2026
@johnnyluo johnnyluo added this pull request to the merge queue May 8, 2026
Merged via the queue into main with commit c4052ef May 8, 2026
2 checks passed
@johnnyluo johnnyluo deleted the agent/4427-crash--illegalargumentexception-in-lazyl branch May 8, 2026 01:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash: IllegalArgumentException in LazyList SubcomposeLayout recurs on Android 16 (Samsung Galaxy A15)

4 participants