Skip to content

fix: optimize home screen display#1473

Open
HashEngineering wants to merge 41 commits intomasterfrom
fix/improve-home-screen-display
Open

fix: optimize home screen display#1473
HashEngineering wants to merge 41 commits intomasterfrom
fix/improve-home-screen-display

Conversation

@HashEngineering
Copy link
Collaborator

@HashEngineering HashEngineering commented Mar 17, 2026

Issue being fixed or feature implemented

Related PR's and Dependencies

Screenshots / Videos

How Has This Been Tested?

  • QA (Mobile Team)

Checklist:

  • I have performed a self-review of my own code and added comments where necessary
  • I have added or updated relevant unit/integration/functional/e2e tests

Summary by CodeRabbit

  • New Features

    • Fast, cache-backed transaction list on startup with seamless swap to live paging; on-demand cache rebuild action.
  • Improvements

    • Persistent transaction grouping and incremental cache updates for smoother paging and faster UI.
    • Updated transaction list header with filter button, syncing indicator, loading/empty overlays, and startup timing logs.
    • Exposed transaction cache APIs so UI reacts faster to state changes.
  • Documentation

    • Proposals added for async wallet loading and transaction-display performance.
  • Tests

    • New instrumented wallet load performance benchmark.
  • Chores

    • Project version and database schema updates; added paging support and migrations.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a cache-first, paging-backed transaction display pipeline with new Room entities/DAOs and migration (16→17), in-memory prebuilt paging and adapter-swap to live PagingData, extensive TxDisplayCacheService, schema snapshots (17–21), UI/layout/strings updates, a performance test, small util/model tweaks, and a build version bump.

Changes

Cohort / File(s) Summary
Database schemas
wallet/schemas/de.schildbach.wallet.database.AppDatabase/17.json, .../18.json, .../19.json, .../20.json, .../21.json
Added Room schema JSON snapshots for DB versions 17–21.
Room DB & migrations
wallet/src/de/schildbach/wallet/database/AppDatabase.kt, wallet/src/de/schildbach/wallet/database/AppDatabaseMigrations.kt
Added TxDisplayCacheEntry and TxGroupCacheEntry to Room entities, exposed DAOs, bumped DB version to 17, and added migration16to17 creating tx_display_cache and tx_group_cache.
Cache entities & DAOs
wallet/src/.../entity/TxDisplayCacheEntry.kt, .../TxGroupCacheEntry.kt, wallet/src/.../dao/TxDisplayCacheDao.kt, .../TxGroupCacheDao.kt
New Room entities and DAOs for persisted transaction display rows and group metadata; DAOs include pagingSource, batch insert/replace, transactional replaceAll, and targeted reads.
Cache service & flow
wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt
New singleton service implementing cache-first in-memory wrapper management, prebuilt cache loading, live Room Pager integration, contact/metadata resolution, persistence of display and group caches, filter handling, on-demand group wrapper loading, and public APIs (transactions Flow, cachedRows, transactionsLoaded, forceRebuildTransactionCache, getTransactionWrapper, loadGroupWrapper).
ViewModel & UI integration
wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt, wallet/src/de/schildbach/wallet/di/DatabaseModule.kt
MainViewModel refactored to depend on TxDisplayCacheService, exposes transactions and transaction-related state, delegates cache rebuild/loading APIs; DatabaseModule provides new DAOs and registers migration.
Adapters & paging source
wallet/src/de/schildbach/wallet/ui/main/TransactionAdapter.kt, .../PrebuiltRowsPagingSource.kt
Extracted shared TransactionViewHolder and HistoryRowDiffCallback; added PagingDataAdapter TransactionAdapter, ListAdapter CacheTransactionAdapter, and in-memory PrebuiltRowsPagingSource for prebuilt rows.
Fragments & UI wiring
wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt, .../WalletFragment.kt, .../TransactionGroupDetailsFragment.kt, wallet/src/de/schildbach/wallet/ui/main/WalletFragment.kt
Dual-adapter ConcatAdapter flow to show cached rows then swap to live paging; added load-state handling, adapter-swap safety, cache-rebuild dialog, startup timing instrumentation, and updated observers to new ViewModel properties.
Layout & strings
wallet/res/layout/wallet_transactions_fragment.xml, wallet/res/values/strings-extra.xml
Reworked transactions fragment layout (header with filter button, title, syncing indicator, overlays) and added strings for transaction-building state and history refresh dialog.
Build & dependencies
build.gradle, wallet/build.gradle
Bumped dashjVersion to 22.0.1-SNAPSHOT; added Room paging and paging runtime dependencies.
Models & utils
common/src/.../ResourceString.kt, common/src/.../RateRetrievalState.kt, wallet/src/.../TransactionRowView.kt
ResourceString: added optional resolvedText and defaulted resourceId; RateRetrievalState: added DEFAULT companion instance; TransactionRowView: added optional statusText.
Startup logging
wallet/src/de/schildbach/wallet/WalletApplication.java
Added finer-grained startup timing logs and duration measurements across init steps; minor toast context fix and injected txDisplayCacheService.
Tests & perf
wallet/androidTest/de/schildbach/wallet/performance/WalletLoadPerformanceTest.kt, wallet/test/.../TransactionExportTest.kt
Added instrumented performance test comparing serial vs parallel wallet loading; adjusted test package/import for TransactionExportTest.
Other UI tweaks
wallet/src/de/schildbach/wallet/ui/dashpay/UpgradeToEvolutionFragment.kt, wallet/src/de/schildbach/wallet/ui/more/MoreFragment.kt, wallet/src/de/schildbach/wallet/ui/main/PrebuiltRowsPagingSource.kt
Small observer/property name alignments to new ViewModel fields and added PrebuiltRowsPagingSource class.
Docs / proposals
docs/proposals/async-wallet-loading.md, docs/proposals/transaction-display-performance.md
Added two design proposals describing async wallet loading and transaction-display performance strategies and roadmap.

Sequence Diagram(s)

sequenceDiagram
    participant UI as WalletTransactionsFragment
    participant VM as MainViewModel
    participant CacheSvc as TxDisplayCacheService
    participant CacheDAO as TxDisplayCacheDao
    participant GroupDAO as TxGroupCacheDao
    participant Paging as Room/Paging
    participant Wallet as Wallet Loader

    UI->>VM: onViewCreated()
    VM->>CacheSvc: request cachedRows()
    CacheSvc->>CacheDAO: load cached TxDisplayCacheEntry list
    CacheDAO-->>CacheSvc: cached entries
    CacheSvc-->>VM: emit cachedRows StateFlow
    VM-->>UI: submit cachedRows to CacheTransactionAdapter (immediate render)

    VM->>CacheSvc: subscribe to transactions Flow
    CacheSvc->>Wallet: observe wallet/metadata/contact flows (background)
    Wallet-->>CacheSvc: live transaction updates
    CacheSvc->>Paging: build Pager (Room) -> PagingData<HistoryRowView>
    Paging-->>CacheSvc: PagingData stream
    CacheSvc-->>VM: emit transactions Flow
    VM-->>UI: submitData to TransactionAdapter -> swap to live adapter
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 Hop, hop — the rows arrive in store,
Cache first, then live, the UI takes flight.
Paging hums, groups tuck in neat,
Startup quicker, the list feels light.
A rabbit claps — cache then live, delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: optimize home screen display' is related to the changeset but is overly broad. The PR includes many changes: transaction caching service, database migrations, paging infrastructure, and UI refactoring. The title doesn't capture the core implementation detail (TxDisplayCacheService singleton for transaction caching).

✏️ 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 fix/improve-home-screen-display
📝 Coding Plan
  • Generate coding plan for human review comments

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
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: 7

🧹 Nitpick comments (15)
docs/proposals/transaction-display-performance.md (2)

23-56: Add a language identifier to the fenced code block.

The code block starting at line 23 lacks a language specifier, which prevents syntax highlighting and can trigger linting warnings.

📝 Suggested fix
-```
+```text
 Data Sources
 ├── walletData.observeTransactions()       → List<Transaction>          (filtered, batched 500ms)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/proposals/transaction-display-performance.md` around lines 23 - 56, The
fenced code block containing the architecture diagram (starting with "Data
Sources" and including symbols like walletData.observeTransactions(),
refreshTransactions(), refreshTransactionBatch(), updateContactsAndMetadata(),
and StateFlow<Map<LocalDate, List<TransactionRowView>>> ) needs a language
identifier to satisfy linting and enable highlighting; update the opening fence
from ``` to ```text (or another appropriate language) so the block becomes
```text and leave the rest unchanged.

327-327: Normalize the horizontal rule style.

The horizontal rule on this line has trailing spaces which is inconsistent with others in the document (e.g., line 7). Consider using a consistent --- format throughout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/proposals/transaction-display-performance.md` at line 327, The
horizontal rule on the indicated line contains trailing spaces; normalize it to
match the other rules by replacing the current line with a clean three-dash rule
("---") with no trailing whitespace so all horizontal rules in
transaction-display-performance.md use the same `---` format.
wallet/build.gradle (1)

124-125: Consider extracting the paging version to an ext variable for consistency.

Other dependencies in this project use version variables defined in the root build.gradle (e.g., $roomVersion, $lifecycleVersion). Hardcoding 3.1.1 here could lead to version drift if paging is used elsewhere or updated later.

♻️ Suggested improvement

In root build.gradle, add:

pagingVersion = '3.1.1'

Then update this line:

-    implementation "androidx.paging:paging-runtime-ktx:3.1.1"
+    implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/build.gradle` around lines 124 - 125, Extract the hardcoded paging
version by adding a pagingVersion property in the root build.gradle (matching
how roomVersion and lifecycleVersion are defined), then replace the literal
version in the module's dependency declaration (implementation
"androidx.paging:paging-runtime-ktx:3.1.1") with the variable reference (e.g.,
using pagingVersion) so the dependency becomes consistent with other
version-managed libs; update any documentation or README if you track dependency
variables centrally.
wallet/src/de/schildbach/wallet/WalletApplication.java (1)

488-539: Consider using Stopwatch for timing consistency.

The file already uses Stopwatch (from Guava) for timing in loadWalletFromProtobuf() and protobufSerializeWallet(). Using the same pattern here would be more consistent and readable than manual System.currentTimeMillis() arithmetic with the inline reassignment pattern.

♻️ Suggested approach
 public void finalizeInitialization() {
-    long _t = System.currentTimeMillis();
+    Stopwatch watch = Stopwatch.createStarted();
     // ... security guard code ...
-    log.info("STARTUP finalizeInit: securityGuard done in {}ms", System.currentTimeMillis() - _t); _t = System.currentTimeMillis();
+    log.info("STARTUP finalizeInit: securityGuard done in {}", watch); watch.reset().start();
 
     dashSystemService.getSystem().initDash(true, true, Constants.SYNC_FLAGS, Constants.VERIFY_FLAGS);
-    log.info("STARTUP finalizeInit: initDash done in {}ms", System.currentTimeMillis() - _t); _t = System.currentTimeMillis();
+    log.info("STARTUP finalizeInit: initDash done in {}", watch); watch.reset().start();
     // ... etc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/WalletApplication.java` around lines 488 -
539, Replace the manual System.currentTimeMillis() timing pattern in
WalletApplication (the long _t variable and its reassignment) with a Guava
Stopwatch for consistency: create a Stopwatch (Stopwatch.createStarted()) before
the securityGuard block, use stopwatch.elapsed(TimeUnit.MILLISECONDS) in the log
statements after the securityGuard ensureMnemonicFallbacks/ensurePinFallback
sequence, after dashSystemService.getSystem().initDash(...), after
afterLoadWallet(), after initPlatform()/createNotificationChannels(), and after
the integrations initUphold(), initCoinbase(), initDashSpend(); between sections
call stopwatch.reset(); stopwatch.start() to restart timing. Add the necessary
import for com.google.common.base.Stopwatch and java.util.concurrent.TimeUnit
and reference the existing methods (ensureMnemonicFallbacks, ensurePinFallback,
initDash, afterLoadWallet, cleanupFiles, createNotificationChannels,
initPlatform, initUphold, initCoinbase, initDashSpend) when replacing the
corresponding timing/log lines.
wallet/androidTest/de/schildbach/wallet/performance/WalletLoadPerformanceTest.kt (3)

162-164: Remove unused error function.

The error helper function is defined but never called in this test class.

🧹 Proposed cleanup
-    fun error(format: String, vararg args: Any?) {
-        log("ERROR", format, *args)
-    }
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@wallet/androidTest/de/schildbach/wallet/performance/WalletLoadPerformanceTest.kt`
around lines 162 - 164, The helper function error(format: String, vararg args:
Any?) in WalletLoadPerformanceTest.kt is unused and should be removed to clean
up dead code; locate the function named error and delete its declaration (and
any associated unused overloads like the log("ERROR", ...) call path) so no
unused helper remains in the test class.

44-48: Remove unused constants.

MNEMONIC and EXPECTED_SAVE_TIME_MS are declared but never used in this test class.

🧹 Proposed cleanup
 companion object {
     private val log = LoggerFactory.getLogger(WalletLoadPerformanceTest::class.java)
-    private const val MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
-    private const val EXPECTED_SAVE_TIME_MS = 5000L // Expected maximum save time
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@wallet/androidTest/de/schildbach/wallet/performance/WalletLoadPerformanceTest.kt`
around lines 44 - 48, Remove the unused constants MNEMONIC and
EXPECTED_SAVE_TIME_MS from the companion object in WalletLoadPerformanceTest;
locate the companion object containing
LoggerFactory.getLogger(WalletLoadPerformanceTest::class.java) and delete the
declarations for MNEMONIC and EXPECTED_SAVE_TIME_MS so the class no longer
contains unused fields.

199-206: Use explicit locale for String.format.

Using implicit default locale can produce inconsistent output across devices/regions. For test logging, this is minor but worth fixing for consistency.

🔧 Proposed fix
+import java.util.Locale
+
 // Inside formatMessage function:
-                            replacement = if (decimals == 1) {
-                                String.format("%.1f", value)
-                            } else if (decimals == 2) {
-                                String.format("%.2f", value)
-                            } else {
-                                String.format("%.2f", value) // default
-                            }
+                            replacement = if (decimals == 1) {
+                                String.format(Locale.US, "%.1f", value)
+                            } else if (decimals == 2) {
+                                String.format(Locale.US, "%.2f", value)
+                            } else {
+                                String.format(Locale.US, "%.2f", value) // default
+                            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@wallet/androidTest/de/schildbach/wallet/performance/WalletLoadPerformanceTest.kt`
around lines 199 - 206, The String.format calls in WalletLoadPerformanceTest.kt
use the implicit default Locale which can vary across devices; update the
formatting to use an explicit Locale (e.g., Locale.US or Locale.ROOT) for the
three String.format invocations that produce replacement (the branches when
decimals == 1, decimals == 2, and the default) so that String.format is called
with a Locale parameter and the same format string, ensuring consistent numeric
output for the replacement variable computed from arg.toDouble().
wallet/src/de/schildbach/wallet/ui/main/TransactionAdapter.kt (1)

274-280: Avoid creating new HistoryRowDiffCallback instances on each comparison.

The DiffCallback inner class instantiates a new HistoryRowDiffCallback for every areItemsTheSame and areContentsTheSame call. This creates unnecessary object allocations during list diffing.

♻️ Proposed fix
+    // Keep for backwards compatibility (TransactionGroupDetailsFragment uses it)
     class DiffCallback : DiffUtil.ItemCallback<HistoryRowView>() {
-        override fun areItemsTheSame(oldItem: HistoryRowView, newItem: HistoryRowView) =
-            HistoryRowDiffCallback().areItemsTheSame(oldItem, newItem)
-        override fun areContentsTheSame(oldItem: HistoryRowView, newItem: HistoryRowView) =
-            HistoryRowDiffCallback().areContentsTheSame(oldItem, newItem)
+        private val delegate = HistoryRowDiffCallback()
+        override fun areItemsTheSame(oldItem: HistoryRowView, newItem: HistoryRowView) =
+            delegate.areItemsTheSame(oldItem, newItem)
+        override fun areContentsTheSame(oldItem: HistoryRowView, newItem: HistoryRowView) =
+            delegate.areContentsTheSame(oldItem, newItem)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/TransactionAdapter.kt` around lines
274 - 280, The DiffCallback inner class currently creates a new
HistoryRowDiffCallback on every call to areItemsTheSame and areContentsTheSame;
change DiffCallback to reuse a single HistoryRowDiffCallback instance (e.g.,
private val historyDiff = HistoryRowDiffCallback()) and delegate both methods to
that instance instead of constructing a new one each time, updating the
implementation of areItemsTheSame and areContentsTheSame to call
historyDiff.areItemsTheSame(...) and historyDiff.areContentsTheSame(...).
wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt (1)

197-203: Update comment to reflect StateFlow usage.

The code now observes isAbleToCreateIdentity (a StateFlow), but the comment still references "LiveData". Consider updating the comment for accuracy.

📝 Proposed fix
     fun initViewModel() {
         viewModel.isAbleToCreateIdentity.observe(this) {
             // empty observer just to trigger data loading
             // viewModel is shared with some fragments keeping the observer active
-            // inside the parent Activity will avoid recreation of relatively complex
-            // isAbleToCreateIdentityData LiveData
+            // inside the parent Activity will avoid recreation of relatively complex
+            // isAbleToCreateIdentity StateFlow
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt` around lines 197 -
203, Update the outdated comment in initViewModel to reference StateFlow instead
of LiveData: clarify that viewModel.isAbleToCreateIdentity is a StateFlow and
that attaching an empty observer in the Activity keeps the shared ViewModel's
StateFlow active across fragments to avoid recreation of the relatively complex
isAbleToCreateIdentity data; mention that the observer is intentionally empty
and is used to trigger data loading and retain the StateFlow subscription.
wallet/src/de/schildbach/wallet/database/AppDatabase.kt (1)

47-67: Drop the unreachable 20/21 schema exports or bump the DB version consistently.

AppDatabase still declares version 19, but this PR also adds wallet/schemas/de.schildbach.wallet.database.AppDatabase/20.json and 21.json. Those snapshots already diverge from the 19 schema (tx_group_cache and filterFlags disappear again), so keeping them makes future Room migration baselines misleading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/database/AppDatabase.kt` around lines 47 -
67, AppDatabase currently declares version = 19 while new schema snapshot files
wallet/schemas/de.schildbach.wallet.database.AppDatabase/20.json and 21.json
exist and diverge (e.g., tx_group_cache and filterFlags differences); either
delete the unreachable 20.json and 21.json schema exports or bump
AppDatabase.version to match the latest snapshot (e.g., 21) and ensure the
entities list in AppDatabase (ExchangeRate, BlockchainState,
TransactionMetadata, AddressMetadata, IconBitmap, GiftCard, DashPayProfile,
DashPayContactRequest, UserAlert, Invitation, TransactionMetadataCacheItem,
TransactionMetadataDocument, UsernameRequest, UsernameVote,
ImportedMasternodeKey, TopUp, TxDisplayCacheEntry, TxGroupCacheEntry) and the DB
schema reflect the fields present in the 20/21 snapshots (restore or remove
tx_group_cache and filterFlags consistently) so Room migration baselines remain
accurate.
wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt (3)

1043-1049: tryInclude always returns true but the interface semantics suggest conditional inclusion.

The TransactionWrapper.tryInclude(t: Transaction) method typically returns whether the transaction was successfully included in the wrapper. For single-tx wrappers, returning true unconditionally is semantically misleading and could cause issues if callers rely on the return value.

Consider returning false or throwing since single-tx wrappers shouldn't accept additional transactions.

♻️ Suggested fix
     private fun createSingleTxWrapper(tx: Transaction): TransactionWrapper = object : TransactionWrapper {
         override val id          = tx.txId.toStringBase58()
         override val transactions = hashMapOf(tx.txId to tx)
         override val groupDate   = tx.updateTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
-        override fun tryInclude(t: Transaction) = true
+        override fun tryInclude(t: Transaction) = false  // Single-tx wrappers don't accept additional txs
         override fun getValue(bag: TransactionBag) = tx.getValue(bag)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt` around lines 1043 -
1049, The createSingleTxWrapper implementation currently has
TransactionWrapper.tryInclude(t: Transaction) always returning true; change it
so single-tx wrappers do not claim to accept additional transactions—have
tryInclude return false (and do not modify transactions or state) for the
single-entry wrapper created by createSingleTxWrapper, so callers relying on the
boolean will correctly see that inclusion failed; update the anonymous
TransactionWrapper in createSingleTxWrapper (the tryInclude override)
accordingly.

170-205: ViewModel uses multiple separate flows instead of a single UIState data class.

Per coding guidelines, ViewModels should consolidate state into a single UIState data class with a private mutable _uiState and public immutable uiState via asStateFlow(). Currently, transaction state is spread across _txDataSource, _transactionsLoaded, cachedRows, and transactions.

This is a significant refactor, so consider addressing it in a follow-up PR to reduce the scope of this change.

As per coding guidelines: "ViewModels should use a single UIState data class rather than multiple separate flows" and "Use StateFlow (not LiveData) for asynchronously updated fields in ViewModels".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt` around lines 170 -
205, State is currently split across _txDataSource, _transactionsLoaded,
cachedRows and transactions; consolidate these into a single UIState data class
and expose it as an immutable StateFlow (private MutableStateFlow _uiState,
public uiState = _uiState.asStateFlow()) so the ViewModel follows the guideline
of one StateFlow for UI state. Create UIState fields for txDataSource (or a
simpler enum), transactionsLoaded, cachedRows and any other visible
transaction-related properties; update places currently observing
cachedRows/_txDataSource/_transactionsLoaded to read from uiState instead and
update _uiState atomically when any of those underlying values change (e.g., in
rebuildWrappedList and relevant setters). Make this a contained follow-up
refactor so behavior is preserved while moving the four separate flows into the
single _uiState/ uiState pattern.

248-258: Mix of LiveData and StateFlow for async state.

_isBlockchainSynced, _blockchainSyncPercentage, _exchangeRate, etc. use MutableLiveData, while _transactionsLoaded, _rateStale, _txDataSource use MutableStateFlow. Per coding guidelines, prefer StateFlow for asynchronously updated fields.

This inconsistency may be inherited from existing code, but consider migrating to StateFlow uniformly in a follow-up.

As per coding guidelines: "Use StateFlow (not LiveData) for asynchronously updated fields in ViewModels".

Also applies to: 266-268

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt` around lines 248 -
258, The ViewModel mixes LiveData and StateFlow for async state; migrate the
async LiveData fields to StateFlow to match the rest of the class: replace
_isBlockchainSynced / isBlockchainSynced, _isBlockchainSyncFailed /
isBlockchainSyncFailed and _blockchainSyncPercentage / blockchainSyncPercentage
(and the similar fields referenced at 266-268) to use
MutableStateFlow<T>(initialValue) for the backing property and expose them as
StateFlow<T> (e.g., StateFlow<Boolean> or StateFlow<Int>) so callers can
collect/observe via Kotlin Flow APIs; update any callers (UI code) to use
collect/collectAsState instead of LiveData observation. Ensure you preserve
types and provide sensible initial values (false/0) when creating the
MutableStateFlow instances.
wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt (1)

291-293: distinctUntilChanged() on loadStateFlow may not behave as expected.

CombinedLoadStates contains multiple nested LoadState objects. Using distinctUntilChanged() compares the entire object by equality, which may still emit frequently due to internal state changes (e.g., endOfPaginationReached toggling). This won't cause bugs but may result in more emissions than intended.

Consider removing it or using a more targeted comparison if you observe excessive recomposition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt` around
lines 291 - 293, The current chain in WalletTransactionsFragment using
liveAdapter.loadStateFlow.distinctUntilChanged().collectLatest { ... } is
comparing entire CombinedLoadStates (which contains nested LoadState fields like
endOfPaginationReached) leading to extra emissions; change this to either remove
distinctUntilChanged() or map to the specific fields you care about (e.g., map {
it.refresh to it.append to it.prepend or a boolean like
it.append.endOfPaginationReached }) and then apply distinctUntilChanged() on
that mapped value before collectLatest, so the collector only reacts to the
targeted state changes.
wallet/src/de/schildbach/wallet/database/dao/TxDisplayCacheDao.kt (1)

49-51: Consider SQLite's IN clause limit for large transaction histories.

Room/SQLite has a default limit of ~999 parameters for IN clauses. If rowIds exceeds this limit (e.g., wallets with thousands of transactions), the query will fail. This is unlikely for targeted metadata updates but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/database/dao/TxDisplayCacheDao.kt` around
lines 49 - 51, The current DAO method getEntriesByIds in TxDisplayCacheDao risks
exceeding SQLite's ~999 parameter limit for large rowIds lists; fix by adding a
batched wrapper (e.g., getEntriesByIdsBatched) in the DAO or repository that
splits the incoming rowIds into chunks under the safe limit (use 900 params per
chunk), calls the existing getEntriesByIds for each chunk inside a single
suspend/transactional flow, aggregates/merges the results, and returns the
combined List<TxDisplayCacheEntry>; reference getEntriesByIds and
TxDisplayCacheDao when locating where to add this batching logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wallet/schemas/de.schildbach.wallet.database.AppDatabase/18.json`:
- Around line 991-1145: The tx_display_cache table definition is missing the
filterFlags column used by TxDisplayCacheDao.pagingSource(); update the
tx_display_cache "createSql" and its "fields" array to add a filterFlags INTEGER
(notNull false or as appropriate) and include it in the schema/primary key block
as needed so the bitwise WHERE (:filterFlag = 0 OR (filterFlags & :filterFlag)
!= 0) can run; additionally add an index entry for tx_display_cache on time DESC
to support pagingSource() and getAll(), and add an index for tx_group_cache on
(groupDate DESC, sortOrder ASC) to support getAll() and getActiveGroups() by
updating the "indices" arrays for those table objects.

In `@wallet/src/de/schildbach/wallet/database/AppDatabaseMigrations.kt`:
- Around line 127-153: migration16to17 is wrongly declared as Migration(16, 21)
which incorrectly spans beyond the intended 16→17 change; update its version
tuple to Migration(16, 17) so Room will apply this migration during the 16→19
upgrade path. Locate the object named migration16to17 and replace the
Migration(16, 21) constructor call with Migration(16, 17) without changing the
migration body that creates tx_display_cache.

In `@wallet/src/de/schildbach/wallet/database/entity/TxDisplayCacheEntry.kt`:
- Around line 170-177: The reconstructed DashPayProfile in
TxDisplayCacheEntry.kt incorrectly sets userId = rowId (transaction hex/group
id); change reconstruction in resolvedContact so it uses a cached contactUserId
instead of rowId—add a new cached field (e.g., contactUserId) to
TxDisplayCacheEntry, persist it when storing contact info, and use that value
when creating DashPayProfile(userId = contactUserId); alternatively, if you
don't want to persist IDs, set the reconstructed profile to a display-only form
(e.g., userId = null/empty) and document that DashPayProfile from
contactUsername/contactDisplayName/contactAvatarUrl must not be used for
identity lookups.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 397-412: Within the _transactionsDirection.flatMapLatest block the
suspend call txDisplayCacheDao.getCount() is being invoked on the upstream flow
context (likely main/UI) which can block the UI; change the logic so that any
database calls (txDisplayCacheDao.getCount() and any other DAO/suspend ops used
in this branch) are executed on Dispatchers.IO (e.g. wrap them with
withContext(Dispatchers.IO) or move to a coroutine launched on IO) before
mutating state; ensure you still set _txDataSource.value =
TxDataSource.RoomLive(...), _transactionsLoaded.value = true, and call
initializeFactoriesFromCache() or rebuildWrappedList(filter) from the collected
flow after the IO call completes so behavior remains identical.
- Around line 499-510: The nested viewModelScope.launch(Dispatchers.IO) creates
a detached coroutine so cache deletions may run concurrently with the wrapper
resets and paging invalidation; replace the nested launch with an awaited IO
context (e.g. withContext(Dispatchers.IO) { txDisplayCacheDao.deleteAll();
txGroupCacheDao.deleteAll() }) or capture and join the Job, so deletions
complete before resetting coinJoinWrapperFactory/crowdNodeWrapperFactory and
calling currentPagingSource?.invalidate(); reference the existing
viewModelScope.launch(Dispatchers.IO), txDisplayCacheDao.deleteAll(),
txGroupCacheDao.deleteAll(), coinJoinWrapperFactory, crowdNodeWrapperFactory,
and currentPagingSource?.invalidate() when making the change.

In `@wallet/src/de/schildbach/wallet/ui/main/PrebuiltRowsPagingSource.kt`:
- Around line 50-53: The prevKey currently returns the current offset which
causes prepend to reload the same page; update the prevKey logic in the
LoadResult.Page creation (where LoadResult.Page is constructed using data =
slice, prevKey = ..., nextKey = ...) so that prevKey points to the previous page
start, e.g. null when offset == 0, otherwise max(0, offset - params.loadSize)
(use params.loadSize as the page size) so the paging library will request the
correct preceding offset instead of reloading the current page.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt`:
- Around line 124-133: The coroutine may call requireActivity() after the
fragment is detached causing IllegalStateException; instead, after obtaining the
wrapper from viewModel.loadGroupWrapper, guard against a destroyed/detached
fragment by checking fragment state or the nullable activity and only show the
details if safe (e.g., use activity?.let { ... } or check isAdded()/isDetached()
before calling TransactionGroupDetailsFragment(wrapper).show(...)); keep the
null-return comment behavior when the fragment is not in a valid state and
ensure viewModel.logEvent(AnalyticsConstants.Home.TRANSACTION_DETAILS) is only
called when you actually proceed to show the fragment.

---

Nitpick comments:
In `@docs/proposals/transaction-display-performance.md`:
- Around line 23-56: The fenced code block containing the architecture diagram
(starting with "Data Sources" and including symbols like
walletData.observeTransactions(), refreshTransactions(),
refreshTransactionBatch(), updateContactsAndMetadata(), and
StateFlow<Map<LocalDate, List<TransactionRowView>>> ) needs a language
identifier to satisfy linting and enable highlighting; update the opening fence
from ``` to ```text (or another appropriate language) so the block becomes
```text and leave the rest unchanged.
- Line 327: The horizontal rule on the indicated line contains trailing spaces;
normalize it to match the other rules by replacing the current line with a clean
three-dash rule ("---") with no trailing whitespace so all horizontal rules in
transaction-display-performance.md use the same `---` format.

In
`@wallet/androidTest/de/schildbach/wallet/performance/WalletLoadPerformanceTest.kt`:
- Around line 162-164: The helper function error(format: String, vararg args:
Any?) in WalletLoadPerformanceTest.kt is unused and should be removed to clean
up dead code; locate the function named error and delete its declaration (and
any associated unused overloads like the log("ERROR", ...) call path) so no
unused helper remains in the test class.
- Around line 44-48: Remove the unused constants MNEMONIC and
EXPECTED_SAVE_TIME_MS from the companion object in WalletLoadPerformanceTest;
locate the companion object containing
LoggerFactory.getLogger(WalletLoadPerformanceTest::class.java) and delete the
declarations for MNEMONIC and EXPECTED_SAVE_TIME_MS so the class no longer
contains unused fields.
- Around line 199-206: The String.format calls in WalletLoadPerformanceTest.kt
use the implicit default Locale which can vary across devices; update the
formatting to use an explicit Locale (e.g., Locale.US or Locale.ROOT) for the
three String.format invocations that produce replacement (the branches when
decimals == 1, decimals == 2, and the default) so that String.format is called
with a Locale parameter and the same format string, ensuring consistent numeric
output for the replacement variable computed from arg.toDouble().

In `@wallet/build.gradle`:
- Around line 124-125: Extract the hardcoded paging version by adding a
pagingVersion property in the root build.gradle (matching how roomVersion and
lifecycleVersion are defined), then replace the literal version in the module's
dependency declaration (implementation
"androidx.paging:paging-runtime-ktx:3.1.1") with the variable reference (e.g.,
using pagingVersion) so the dependency becomes consistent with other
version-managed libs; update any documentation or README if you track dependency
variables centrally.

In `@wallet/src/de/schildbach/wallet/database/AppDatabase.kt`:
- Around line 47-67: AppDatabase currently declares version = 19 while new
schema snapshot files
wallet/schemas/de.schildbach.wallet.database.AppDatabase/20.json and 21.json
exist and diverge (e.g., tx_group_cache and filterFlags differences); either
delete the unreachable 20.json and 21.json schema exports or bump
AppDatabase.version to match the latest snapshot (e.g., 21) and ensure the
entities list in AppDatabase (ExchangeRate, BlockchainState,
TransactionMetadata, AddressMetadata, IconBitmap, GiftCard, DashPayProfile,
DashPayContactRequest, UserAlert, Invitation, TransactionMetadataCacheItem,
TransactionMetadataDocument, UsernameRequest, UsernameVote,
ImportedMasternodeKey, TopUp, TxDisplayCacheEntry, TxGroupCacheEntry) and the DB
schema reflect the fields present in the 20/21 snapshots (restore or remove
tx_group_cache and filterFlags consistently) so Room migration baselines remain
accurate.

In `@wallet/src/de/schildbach/wallet/database/dao/TxDisplayCacheDao.kt`:
- Around line 49-51: The current DAO method getEntriesByIds in TxDisplayCacheDao
risks exceeding SQLite's ~999 parameter limit for large rowIds lists; fix by
adding a batched wrapper (e.g., getEntriesByIdsBatched) in the DAO or repository
that splits the incoming rowIds into chunks under the safe limit (use 900 params
per chunk), calls the existing getEntriesByIds for each chunk inside a single
suspend/transactional flow, aggregates/merges the results, and returns the
combined List<TxDisplayCacheEntry>; reference getEntriesByIds and
TxDisplayCacheDao when locating where to add this batching logic.

In `@wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt`:
- Around line 197-203: Update the outdated comment in initViewModel to reference
StateFlow instead of LiveData: clarify that viewModel.isAbleToCreateIdentity is
a StateFlow and that attaching an empty observer in the Activity keeps the
shared ViewModel's StateFlow active across fragments to avoid recreation of the
relatively complex isAbleToCreateIdentity data; mention that the observer is
intentionally empty and is used to trigger data loading and retain the StateFlow
subscription.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 1043-1049: The createSingleTxWrapper implementation currently has
TransactionWrapper.tryInclude(t: Transaction) always returning true; change it
so single-tx wrappers do not claim to accept additional transactions—have
tryInclude return false (and do not modify transactions or state) for the
single-entry wrapper created by createSingleTxWrapper, so callers relying on the
boolean will correctly see that inclusion failed; update the anonymous
TransactionWrapper in createSingleTxWrapper (the tryInclude override)
accordingly.
- Around line 170-205: State is currently split across _txDataSource,
_transactionsLoaded, cachedRows and transactions; consolidate these into a
single UIState data class and expose it as an immutable StateFlow (private
MutableStateFlow _uiState, public uiState = _uiState.asStateFlow()) so the
ViewModel follows the guideline of one StateFlow for UI state. Create UIState
fields for txDataSource (or a simpler enum), transactionsLoaded, cachedRows and
any other visible transaction-related properties; update places currently
observing cachedRows/_txDataSource/_transactionsLoaded to read from uiState
instead and update _uiState atomically when any of those underlying values
change (e.g., in rebuildWrappedList and relevant setters). Make this a contained
follow-up refactor so behavior is preserved while moving the four separate flows
into the single _uiState/ uiState pattern.
- Around line 248-258: The ViewModel mixes LiveData and StateFlow for async
state; migrate the async LiveData fields to StateFlow to match the rest of the
class: replace _isBlockchainSynced / isBlockchainSynced, _isBlockchainSyncFailed
/ isBlockchainSyncFailed and _blockchainSyncPercentage /
blockchainSyncPercentage (and the similar fields referenced at 266-268) to use
MutableStateFlow<T>(initialValue) for the backing property and expose them as
StateFlow<T> (e.g., StateFlow<Boolean> or StateFlow<Int>) so callers can
collect/observe via Kotlin Flow APIs; update any callers (UI code) to use
collect/collectAsState instead of LiveData observation. Ensure you preserve
types and provide sensible initial values (false/0) when creating the
MutableStateFlow instances.

In `@wallet/src/de/schildbach/wallet/ui/main/TransactionAdapter.kt`:
- Around line 274-280: The DiffCallback inner class currently creates a new
HistoryRowDiffCallback on every call to areItemsTheSame and areContentsTheSame;
change DiffCallback to reuse a single HistoryRowDiffCallback instance (e.g.,
private val historyDiff = HistoryRowDiffCallback()) and delegate both methods to
that instance instead of constructing a new one each time, updating the
implementation of areItemsTheSame and areContentsTheSame to call
historyDiff.areItemsTheSame(...) and historyDiff.areContentsTheSame(...).

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt`:
- Around line 291-293: The current chain in WalletTransactionsFragment using
liveAdapter.loadStateFlow.distinctUntilChanged().collectLatest { ... } is
comparing entire CombinedLoadStates (which contains nested LoadState fields like
endOfPaginationReached) leading to extra emissions; change this to either remove
distinctUntilChanged() or map to the specific fields you care about (e.g., map {
it.refresh to it.append to it.prepend or a boolean like
it.append.endOfPaginationReached }) and then apply distinctUntilChanged() on
that mapped value before collectLatest, so the collector only reacts to the
targeted state changes.

In `@wallet/src/de/schildbach/wallet/WalletApplication.java`:
- Around line 488-539: Replace the manual System.currentTimeMillis() timing
pattern in WalletApplication (the long _t variable and its reassignment) with a
Guava Stopwatch for consistency: create a Stopwatch (Stopwatch.createStarted())
before the securityGuard block, use stopwatch.elapsed(TimeUnit.MILLISECONDS) in
the log statements after the securityGuard
ensureMnemonicFallbacks/ensurePinFallback sequence, after
dashSystemService.getSystem().initDash(...), after afterLoadWallet(), after
initPlatform()/createNotificationChannels(), and after the integrations
initUphold(), initCoinbase(), initDashSpend(); between sections call
stopwatch.reset(); stopwatch.start() to restart timing. Add the necessary import
for com.google.common.base.Stopwatch and java.util.concurrent.TimeUnit and
reference the existing methods (ensureMnemonicFallbacks, ensurePinFallback,
initDash, afterLoadWallet, cleanupFiles, createNotificationChannels,
initPlatform, initUphold, initCoinbase, initDashSpend) when replacing the
corresponding timing/log lines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f59eace6-a71a-4bde-b889-a56ef7700e5e

📥 Commits

Reviewing files that changed from the base of the PR and between 1a438df and 0bc9093.

📒 Files selected for processing (33)
  • build.gradle
  • common/src/main/java/org/dash/wallet/common/services/RateRetrievalState.kt
  • common/src/main/java/org/dash/wallet/common/util/ResourceString.kt
  • docs/proposals/async-wallet-loading.md
  • docs/proposals/transaction-display-performance.md
  • wallet/androidTest/de/schildbach/wallet/performance/WalletLoadPerformanceTest.kt
  • wallet/build.gradle
  • wallet/res/values/strings-extra.xml
  • wallet/schemas/de.schildbach.wallet.database.AppDatabase/17.json
  • wallet/schemas/de.schildbach.wallet.database.AppDatabase/18.json
  • wallet/schemas/de.schildbach.wallet.database.AppDatabase/19.json
  • wallet/schemas/de.schildbach.wallet.database.AppDatabase/20.json
  • wallet/schemas/de.schildbach.wallet.database.AppDatabase/21.json
  • wallet/src/de/schildbach/wallet/WalletApplication.java
  • wallet/src/de/schildbach/wallet/database/AppDatabase.kt
  • wallet/src/de/schildbach/wallet/database/AppDatabaseMigrations.kt
  • wallet/src/de/schildbach/wallet/database/dao/TxDisplayCacheDao.kt
  • wallet/src/de/schildbach/wallet/database/dao/TxGroupCacheDao.kt
  • wallet/src/de/schildbach/wallet/database/entity/TxDisplayCacheEntry.kt
  • wallet/src/de/schildbach/wallet/database/entity/TxGroupCacheEntry.kt
  • wallet/src/de/schildbach/wallet/di/DatabaseModule.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/UpgradeToEvolutionFragment.kt
  • wallet/src/de/schildbach/wallet/ui/dashpay/UserAlert.kt
  • wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt
  • wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/main/PrebuiltRowsPagingSource.kt
  • wallet/src/de/schildbach/wallet/ui/main/TransactionAdapter.kt
  • wallet/src/de/schildbach/wallet/ui/main/WalletFragment.kt
  • wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt
  • wallet/src/de/schildbach/wallet/ui/more/MoreFragment.kt
  • wallet/src/de/schildbach/wallet/ui/transactions/TransactionGroupDetailsFragment.kt
  • wallet/src/de/schildbach/wallet/ui/transactions/TransactionRowView.kt
  • wallet/test/de/schildbach/wallet/ui/more/tools/TransactionExportTest.kt

@HashEngineering HashEngineering changed the title Fix/improve home screen display fix: optimize home screen display Mar 17, 2026
Copy link
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

Caution

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

⚠️ Outside diff range comments (1)
wallet/src/de/schildbach/wallet/transactions/WalletBalanceObserver.kt (1)

70-80: ⚠️ Potential issue | 🟠 Major

Wallet reset handling is incomplete for observe() subscribers

Line 80 adds reset handling for _totalBalance/_mixedBalance, but observe() listeners (custom balance flow) still don’t subscribe to reset events. After a wallet reset, those subscribers can stay stale until another tx/change event occurs.

🔧 Proposed fix (add reset listener inside observe())
     val walletChangeListener = object : ThrottlingWalletChangeListener() {
         override fun onThrottledWalletChanged() {
             emitBalance()
         }
     }
+    val walletResetListener = object : WalletResetEventListener {
+        override fun onWalletReset(wallet: Wallet?) {
+            emitBalance()
+        }
+    }

     wallet.addChangeEventListener(Threading.SAME_THREAD, walletChangeListener)
     wallet.addCoinsSentEventListener(Threading.SAME_THREAD, walletChangeListener)
     wallet.addCoinsReceivedEventListener(Threading.SAME_THREAD, walletChangeListener)
+    wallet.addResetEventListener(Threading.SAME_THREAD, walletResetListener)

     emitBalance()

     awaitClose {
         wallet.removeChangeEventListener(walletChangeListener)
         wallet.removeCoinsSentEventListener(walletChangeListener)
         wallet.removeCoinsReceivedEventListener(walletChangeListener)
+        wallet.removeResetEventListener(walletResetListener)
         walletChangeListener.removeCallbacks()
         emitterJob.cancel()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/transactions/WalletBalanceObserver.kt` around
lines 70 - 80, The custom observe() flow doesn't subscribe to wallet reset
events, so subscribers can remain stale after a Wallet reset; update the
observe() implementation to also register walletResetListener via
wallet.addResetEventListener(Threading.SAME_THREAD, walletResetListener) when a
collector starts and ensure you remove/unregister that listener on cancellation
(mirroring how walletChangeListener is added/removed) so emitBalances()/the
custom balance emission runs on reset for observe() subscribers.
♻️ Duplicate comments (1)
wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt (1)

518-521: ⚠️ Potential issue | 🟠 Major

Await cache deletion during wallet reset to avoid race windows.

Line 518–521 launches deletion in a detached coroutine, but Line 526 invalidates paging immediately. This can interleave reset, invalidation, and incoming updates nondeterministically.

🔧 Proposed fix
+import kotlinx.coroutines.withContext
...
         walletData.observeWalletReset()
             .onEach {
                 wrappedTransactionList = emptyList()
                 contactsByTxId = mapOf()
-                viewModelScope.launch(Dispatchers.IO) {
+                withContext(Dispatchers.IO) {
                     txDisplayCacheDao.deleteAll()
                     txGroupCacheDao.deleteAll()
                 }
                 walletData.wallet?.let { wallet ->
                     coinJoinWrapperFactory = CoinJoinTxWrapperFactory(walletData.networkParameters, wallet as WalletEx)
                     crowdNodeWrapperFactory = FullCrowdNodeSignUpTxSetFactory(walletData.networkParameters, wallet)
                 }
                 currentPagingSource?.invalidate()
             }

Also applies to: 526-527

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt` around lines 518 -
521, The cache deletions are launched in a detached IO coroutine
(viewModelScope.launch(Dispatchers.IO) { txDisplayCacheDao.deleteAll();
txGroupCacheDao.deleteAll() }) which can run after you immediately invalidate
paging, causing race windows; change this to perform the deletions synchronously
from the caller coroutine so you await completion before invalidating: e.g. make
the surrounding function suspend or use viewModelScope.launch {
withContext(Dispatchers.IO) { txDisplayCacheDao.deleteAll();
txGroupCacheDao.deleteAll() } ; /* then call the existing paging invalidation
code */ } so the deleteAll() calls finish before you call the invalidation logic
referenced around the current lines.
🧹 Nitpick comments (1)
wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt (1)

338-347: Consolidate ViewModel state into a single uiState to avoid split-state drift.

This adds another independently exposed async state while isAbleToCreateIdentityLiveData still exists, increasing duplicate state surfaces and update paths.

As per coding guidelines, **/*ViewModel.kt: “ViewModels should use a single UIState data class rather than multiple separate flows”; “Use StateFlow (not LiveData) for asynchronously updated fields in ViewModels”; and “Use private mutable _uiState with public immutable uiState via asStateFlow() in ViewModels`.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt` around lines 338 -
347, The ViewModel exposes a separate StateFlow isAbleToCreateIdentity (and
still has isAbleToCreateIdentityLiveData), causing split-state drift; replace
these by consolidating into a single UI state: create a private
MutableStateFlow<_uiState> and public uiState = _uiState.asStateFlow(), define a
UIState data class including isAbleToCreateIdentity and any other fields (e.g.,
blockchainIdentity, isPlatformAvailable), compute isAbleToCreateIdentity by
combining isPlatformAvailable and blockchainIdentity (use the existing combine/
combineLatestData logic) and update _uiState inside viewModelScope, and remove
the independent isAbleToCreateIdentity StateFlow/LiveData exposures so all async
updates flow through the single uiState.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wallet/schemas/de.schildbach.wallet.database.AppDatabase/17.json`:
- Line 993: The Room schema shows a mismatch for the filterFlags column: the
created table SQL in AppDatabase schema JSON declares "filterFlags INTEGER NOT
NULL" while the migration SQL in AppDatabaseMigrations (16→17) adds it with
"DEFAULT 0", and the Kotlin entity has a Kotlin default but lacks an explicit
Room default; fix by either adding `@ColumnInfo`(defaultValue = "0") to the
entity's filterFlags property and regenerating the schema (ensuring the entity
name and property are the one named filterFlags), or remove the "DEFAULT 0"
clause from the ALTER TABLE / ADD COLUMN SQL in AppDatabaseMigrations.kt (the
migration that targets version 17) so both migration and entity/schema stay
consistent.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 1060-1063: The single-tx wrapper uses Base58 for the id which is
inconsistent with the rest of the flow that keys by hex; update the
createSingleTxWrapper implementation so TransactionWrapper.id uses the hex
string (tx.txId.toString()) instead of tx.txId.toStringBase58(), keeping
transactions and groupDate as-is to preserve the existing map key and date
behavior (refer to createSingleTxWrapper, TransactionWrapper, and the id
property).

---

Outside diff comments:
In `@wallet/src/de/schildbach/wallet/transactions/WalletBalanceObserver.kt`:
- Around line 70-80: The custom observe() flow doesn't subscribe to wallet reset
events, so subscribers can remain stale after a Wallet reset; update the
observe() implementation to also register walletResetListener via
wallet.addResetEventListener(Threading.SAME_THREAD, walletResetListener) when a
collector starts and ensure you remove/unregister that listener on cancellation
(mirroring how walletChangeListener is added/removed) so emitBalances()/the
custom balance emission runs on reset for observe() subscribers.

---

Duplicate comments:
In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 518-521: The cache deletions are launched in a detached IO
coroutine (viewModelScope.launch(Dispatchers.IO) {
txDisplayCacheDao.deleteAll(); txGroupCacheDao.deleteAll() }) which can run
after you immediately invalidate paging, causing race windows; change this to
perform the deletions synchronously from the caller coroutine so you await
completion before invalidating: e.g. make the surrounding function suspend or
use viewModelScope.launch { withContext(Dispatchers.IO) {
txDisplayCacheDao.deleteAll(); txGroupCacheDao.deleteAll() } ; /* then call the
existing paging invalidation code */ } so the deleteAll() calls finish before
you call the invalidation logic referenced around the current lines.

---

Nitpick comments:
In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 338-347: The ViewModel exposes a separate StateFlow
isAbleToCreateIdentity (and still has isAbleToCreateIdentityLiveData), causing
split-state drift; replace these by consolidating into a single UI state: create
a private MutableStateFlow<_uiState> and public uiState =
_uiState.asStateFlow(), define a UIState data class including
isAbleToCreateIdentity and any other fields (e.g., blockchainIdentity,
isPlatformAvailable), compute isAbleToCreateIdentity by combining
isPlatformAvailable and blockchainIdentity (use the existing combine/
combineLatestData logic) and update _uiState inside viewModelScope, and remove
the independent isAbleToCreateIdentity StateFlow/LiveData exposures so all async
updates flow through the single uiState.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5ecf195-7146-4c22-b6d6-257b43eee6ae

📥 Commits

Reviewing files that changed from the base of the PR and between 0bc9093 and 963927b.

📒 Files selected for processing (8)
  • wallet/schemas/de.schildbach.wallet.database.AppDatabase/17.json
  • wallet/schemas/de.schildbach.wallet.database.AppDatabase/20.json
  • wallet/src/de/schildbach/wallet/database/AppDatabase.kt
  • wallet/src/de/schildbach/wallet/database/AppDatabaseMigrations.kt
  • wallet/src/de/schildbach/wallet/database/entity/TxDisplayCacheEntry.kt
  • wallet/src/de/schildbach/wallet/di/DatabaseModule.kt
  • wallet/src/de/schildbach/wallet/transactions/WalletBalanceObserver.kt
  • wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • wallet/src/de/schildbach/wallet/database/AppDatabaseMigrations.kt
  • wallet/schemas/de.schildbach.wallet.database.AppDatabase/20.json

Copy link
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: 4

♻️ Duplicate comments (1)
wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt (1)

524-538: ⚠️ Potential issue | 🟠 Major

Await cache deletes during wallet reset; current detached launch can race with updates.

At Line 528, viewModelScope.launch(Dispatchers.IO) is detached from the reset flow. Deletes can run concurrently with wrapper resets and paging invalidation, causing inconsistent cache state.

🛠️ Suggested synchronization fix
         walletData.observeWalletReset()
             .onEach {
                 wrappedTransactionList = emptyList()
                 contactsByTxId = mapOf()
-                viewModelScope.launch(Dispatchers.IO) {
-                    txDisplayCacheDao.deleteAll()
-                    txGroupCacheDao.deleteAll()
-                }
+                txDisplayCacheDao.deleteAll()
+                txGroupCacheDao.deleteAll()
                 walletData.wallet?.let { wallet ->
                     coinJoinWrapperFactory = CoinJoinTxWrapperFactory(walletData.networkParameters, wallet as WalletEx)
                     crowdNodeWrapperFactory = FullCrowdNodeSignUpTxSetFactory(walletData.networkParameters, wallet)
                 }
                 currentPagingSource?.invalidate()
             }
-            .launchIn(viewModelScope)
+            .launchIn(viewModelWorkerScope)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt` around lines 524 -
538, The wallet reset handler launches cache deletes in a detached
viewModelScope coroutine which can race with wrapper resets and paging
invalidation; instead perform the deletes synchronously in the reset flow by
awaiting them (e.g. call txDisplayCacheDao.deleteAll() and
txGroupCacheDao.deleteAll() from the current coroutine using
withContext(Dispatchers.IO) or make the onEach lambda suspend) before
reassigning wrappedTransactionList/contactsByTxId, recreating
coinJoinWrapperFactory and crowdNodeWrapperFactory, and calling
currentPagingSource?.invalidate(), so the deletes complete before the rest of
the reset proceeds.
🧹 Nitpick comments (1)
wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt (1)

316-324: Use a single loading-visibility render path.

binding.loading.isVisible is currently written by both load-state handling and isBuildingCache observation. Unifying this into one computed path will prevent UI flicker and state overrides.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt` around
lines 316 - 324, The fragment currently sets binding.loading.isVisible in two
places causing race/flicker: the load-state handling block and the
viewModel.isBuildingCache observer; consolidate to a single computed render path
inside WalletTransactionsFragment by deriving a single Boolean (e.g., val
loadingVisible = isLoading || viewModel.isBuildingCache.value) and use that to
set binding.loading.isVisible only once where load-state is processed (keep the
existing showEmptyView()/showTransactionList() logic intact), and remove the
separate viewModel.isBuildingCache.observe that sets binding.loading.isVisible
to avoid conflicting updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wallet/src/de/schildbach/wallet/database/AppDatabaseMigrations.kt`:
- Around line 156-165: Add a non-unique index on tx_group_cache.txId to optimize
queries that start from txId (used by getGroupsForTxIds). In the migration that
creates the tx_group_cache table (in AppDatabaseMigrations.kt), execute an
additional SQL statement like CREATE INDEX IF NOT EXISTS idx_tx_group_cache_txId
ON tx_group_cache(txId) immediately after the CREATE TABLE to ensure the index
is present for existing and new databases. Ensure the index name is unique and
matches the project's naming conventions.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 172-213: The ViewModel currently exposes multiple parallel flows
(transactionsLoaded, _isBuildingCache/isBuildingCache, cachedRows derived from
_txDataSource, etc.); refactor to a single MainUiState data class and one
private MutableStateFlow _uiState with public uiState via asStateFlow(),
populate fields (transactionsLoaded, isBuildingCache, cachedRows,
canCreateIdentity, ...) and update all places that read
transactionsLoaded/isBuildingCache/cachedRows to read from uiState instead;
ensure you derive cachedRows from the existing _txDataSource when building the
initial ui state and update _uiState whenever those underlying sources change
(e.g., when toggling _isBuildingCache or when _txDataSource emits a
PrebuiltCache) so consumers of transactions/transactionsLoaded refer to uiState
properties rather than separate flows.

In `@wallet/src/de/schildbach/wallet/ui/main/TransactionAdapter.kt`:
- Around line 157-163: In setPrimaryStatus(txView: TransactionRowView) ensure
you clear the recycled view when neither txView.contact nor txView.title is
present: after the existing branches add a fallback that resets
binding.primaryStatus.text (e.g., to an empty string) so previous row text
cannot "bleed" into this holder; reference the setPrimaryStatus function,
txView.contact, txView.title and binding.primaryStatus when making the change.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt`:
- Around line 288-305: The problem is that cacheHasItems uses
cacheAdapter.currentList which remains non-empty after cacheAdapter is removed,
hiding loading/empty states; change cacheHasItems to reflect whether the cache
adapter is actually attached to the UI (e.g., check
concatAdapter.adapters.contains(cacheAdapter) and/or
cacheAdapter.isAttachedToRecyclerView) and itemCount, e.g., compute
cacheHasItems = (concatAdapter.adapters.contains(cacheAdapter) &&
cacheAdapter.currentList.isNotEmpty()) or use cacheAdapter.itemCount when
attached; update any logic that relies on cacheHasItems (isLoading/isEmpty) so
it only considers cache content when the cacheAdapter is currently part of
concatAdapter.

---

Duplicate comments:
In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 524-538: The wallet reset handler launches cache deletes in a
detached viewModelScope coroutine which can race with wrapper resets and paging
invalidation; instead perform the deletes synchronously in the reset flow by
awaiting them (e.g. call txDisplayCacheDao.deleteAll() and
txGroupCacheDao.deleteAll() from the current coroutine using
withContext(Dispatchers.IO) or make the onEach lambda suspend) before
reassigning wrappedTransactionList/contactsByTxId, recreating
coinJoinWrapperFactory and crowdNodeWrapperFactory, and calling
currentPagingSource?.invalidate(), so the deletes complete before the rest of
the reset proceeds.

---

Nitpick comments:
In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt`:
- Around line 316-324: The fragment currently sets binding.loading.isVisible in
two places causing race/flicker: the load-state handling block and the
viewModel.isBuildingCache observer; consolidate to a single computed render path
inside WalletTransactionsFragment by deriving a single Boolean (e.g., val
loadingVisible = isLoading || viewModel.isBuildingCache.value) and use that to
set binding.loading.isVisible only once where load-state is processed (keep the
existing showEmptyView()/showTransactionList() logic intact), and remove the
separate viewModel.isBuildingCache.observe that sets binding.loading.isVisible
to avoid conflicting updates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 649d27f1-1afd-4690-8355-688d3bc6a3f5

📥 Commits

Reviewing files that changed from the base of the PR and between 963927b and acda65c.

📒 Files selected for processing (8)
  • wallet/res/layout/wallet_transactions_fragment.xml
  • wallet/res/values/strings-extra.xml
  • wallet/src/de/schildbach/wallet/database/AppDatabaseMigrations.kt
  • wallet/src/de/schildbach/wallet/database/entity/TxDisplayCacheEntry.kt
  • wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/main/PrebuiltRowsPagingSource.kt
  • wallet/src/de/schildbach/wallet/ui/main/TransactionAdapter.kt
  • wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt
✅ Files skipped from review due to trivial changes (1)
  • wallet/res/layout/wallet_transactions_fragment.xml
🚧 Files skipped from review as they are similar to previous changes (2)
  • wallet/res/values/strings-extra.xml
  • wallet/src/de/schildbach/wallet/ui/main/PrebuiltRowsPagingSource.kt

Comment on lines +156 to +165
CREATE TABLE IF NOT EXISTS `tx_group_cache` (
`groupId` TEXT NOT NULL,
`txId` TEXT NOT NULL,
`wrapperType` TEXT NOT NULL,
`groupDate` TEXT NOT NULL,
`sortOrder` INTEGER NOT NULL,
PRIMARY KEY(`groupId`, `txId`)
)
"""
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add an index for tx_group_cache.txId to avoid scan-heavy group lookups.

PRIMARY KEY(groupId, txId) does not optimize queries that start from txId. The lazy-loading paths that call getGroupsForTxIds(...) will degrade as cache size grows.

⚡ Suggested migration update
                 database.execSQL(
                     """
                     CREATE TABLE IF NOT EXISTS `tx_group_cache` (
                         `groupId` TEXT NOT NULL,
                         `txId` TEXT NOT NULL,
                         `wrapperType` TEXT NOT NULL,
                         `groupDate` TEXT NOT NULL,
                         `sortOrder` INTEGER NOT NULL,
                         PRIMARY KEY(`groupId`, `txId`)
                     )
                     """
                 )
+                database.execSQL(
+                    "CREATE INDEX IF NOT EXISTS `index_tx_group_cache_txId` ON `tx_group_cache`(`txId`)"
+                )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE IF NOT EXISTS `tx_group_cache` (
`groupId` TEXT NOT NULL,
`txId` TEXT NOT NULL,
`wrapperType` TEXT NOT NULL,
`groupDate` TEXT NOT NULL,
`sortOrder` INTEGER NOT NULL,
PRIMARY KEY(`groupId`, `txId`)
)
"""
)
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `tx_group_cache` (
`groupId` TEXT NOT NULL,
`txId` TEXT NOT NULL,
`wrapperType` TEXT NOT NULL,
`groupDate` TEXT NOT NULL,
`sortOrder` INTEGER NOT NULL,
PRIMARY KEY(`groupId`, `txId`)
)
"""
)
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_tx_group_cache_txId` ON `tx_group_cache`(`txId`)"
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/database/AppDatabaseMigrations.kt` around
lines 156 - 165, Add a non-unique index on tx_group_cache.txId to optimize
queries that start from txId (used by getGroupsForTxIds). In the migration that
creates the tx_group_cache table (in AppDatabaseMigrations.kt), execute an
additional SQL statement like CREATE INDEX IF NOT EXISTS idx_tx_group_cache_txId
ON tx_group_cache(txId) immediately after the CREATE TABLE to ensure the index
is present for existing and new databases. Ensure the index name is unique and
matches the project's naming conventions.

Comment on lines +172 to +213
// In-memory sorted wrapped list — rebuilt when any source changes
private var wrappedTransactionList: List<TransactionWrapper> = emptyList()

// Simple flag / flow for WalletFragment and WalletTransactionsFragment
private val _transactionsLoaded = MutableStateFlow(false)
val transactionsLoaded: StateFlow<Boolean> = _transactionsLoaded

/** True while a full cache rebuild is in progress (first run or user-initiated refresh). */
private val _isBuildingCache = MutableStateFlow(false)
val isBuildingCache: StateFlow<Boolean> = _isBuildingCache

private val pagingConfig = PagingConfig(pageSize = 50, prefetchDistance = 20, enablePlaceholders = false)
// The current active PagingSource — kept so contact changes can trigger manual invalidate().
// Only used for the RoomLive paging source; PrebuiltCache uses a separate in-memory source.
private var currentPagingSource: PagingSource<Int, TxDisplayCacheEntry>? = null

/**
* Two-phase data source for the transaction list:
* - [TxDataSource.Empty]: no data yet (ViewModel just created, cache not loaded)
* - [TxDataSource.PrebuiltCache]: initial fast display — rows already include date-header
* entries interleaved, so no [insertSeparators] transform is needed on this path
* - [TxDataSource.RoomLive]: live Room-backed paging after [rebuildWrappedList] writes
* fresh data; Room's [InvalidationTracker] auto-invalidates on metadata/tx changes
*/
private sealed class TxDataSource {
object Empty : TxDataSource()
class PrebuiltCache(val rows: List<HistoryRowView>) : TxDataSource()
data class RoomLive(val filterFlag: Int) : TxDataSource()
}
private val _txDataSource = MutableStateFlow<TxDataSource>(TxDataSource.Empty)

/**
* Cache rows for the fast startup phase, exposed so [WalletTransactionsFragment] can call
* [CacheTransactionAdapter.submitList] directly — a single background DiffUtil + one
* main-thread handler post — instead of going through [PagingDataAdapter.submitData]'s
* multi-dispatch coroutine chain which is slow when the main looper is congested at startup.
*/
val cachedRows: StateFlow<List<HistoryRowView>> = _txDataSource
.map { source -> if (source is TxDataSource.PrebuiltCache) source.rows else emptyList() }
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())

val transactions: Flow<PagingData<HistoryRowView>> = _txDataSource
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Consolidate the newly added ViewModel state into a single UIState flow.

This change adds more parallel state channels instead of a unified uiState, which makes consistency and rendering transitions harder to reason about.

♻️ Direction for alignment
data class MainUiState(
    val transactionsLoaded: Boolean = false,
    val isBuildingCache: Boolean = false,
    val cachedRows: List<HistoryRowView> = emptyList(),
    val canCreateIdentity: Boolean = false,
    // ...
)

private val _uiState = MutableStateFlow(MainUiState())
val uiState = _uiState.asStateFlow()

As per coding guidelines, "ViewModels should use a single UIState data class rather than multiple separate flows" and "Use private mutable _uiState with public immutable uiState via asStateFlow() in ViewModels".

Also applies to: 344-353

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt` around lines 172 -
213, The ViewModel currently exposes multiple parallel flows
(transactionsLoaded, _isBuildingCache/isBuildingCache, cachedRows derived from
_txDataSource, etc.); refactor to a single MainUiState data class and one
private MutableStateFlow _uiState with public uiState via asStateFlow(),
populate fields (transactionsLoaded, isBuildingCache, cachedRows,
canCreateIdentity, ...) and update all places that read
transactionsLoaded/isBuildingCache/cachedRows to read from uiState instead;
ensure you derive cachedRows from the existing _txDataSource when building the
initial ui state and update _uiState whenever those underlying sources change
(e.g., when toggling _isBuildingCache or when _txDataSource emits a
PrebuiltCache) so consumers of transactions/transactionsLoaded refer to uiState
properties rather than separate flows.

Comment on lines +157 to 163
private fun setPrimaryStatus(txView: TransactionRowView) {
if (txView.contact != null) {
val name = txView.contact.displayName.ifEmpty { txView.contact.username }
binding.primaryStatus.text = name
} else if (txView.title != null) {
binding.primaryStatus.text = txView.title!!.format(resources)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reset primaryStatus when no contact/title is available to avoid recycled text bleed.

If a recycled holder reaches this branch with both fields absent, prior row text can remain visible.

✅ Minimal safe fix
     private fun setPrimaryStatus(txView: TransactionRowView) {
         if (txView.contact != null) {
             val name = txView.contact.displayName.ifEmpty { txView.contact.username }
             binding.primaryStatus.text = name
         } else if (txView.title != null) {
             binding.primaryStatus.text = txView.title!!.format(resources)
+        } else {
+            binding.primaryStatus.text = null
         }

         binding.primaryStatus.setTextColor(contentColor)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/TransactionAdapter.kt` around lines
157 - 163, In setPrimaryStatus(txView: TransactionRowView) ensure you clear the
recycled view when neither txView.contact nor txView.title is present: after the
existing branches add a fallback that resets binding.primaryStatus.text (e.g.,
to an empty string) so previous row text cannot "bleed" into this holder;
reference the setPrimaryStatus function, txView.contact, txView.title and
binding.primaryStatus when making the change.

Comment on lines +288 to +305
val cacheHasItems = cacheAdapter.currentList.isNotEmpty()
// Only show the loading spinner when there's nothing to display yet.
val isLoading = isRefreshing && liveAdapter.itemCount == 0 && !cacheHasItems
val isEmpty = loadStates.refresh is LoadState.NotLoading &&
liveAdapter.itemCount == 0 && !cacheHasItems

// Swap cacheAdapter → liveAdapter inside the same ConcatAdapter once live
// items are ready. Keeping one ConcatAdapter instance avoids the
// "cannot find wrapper" crash from ConcatAdapterController.
if (loadStates.refresh is LoadState.NotLoading && liveAdapter.itemCount > 0 &&
concatAdapter.adapters.contains(cacheAdapter)) {
log.info("STARTUP swapping to live adapter: {} items at {}",
liveAdapter.itemCount, System.currentTimeMillis())
val lm = binding.walletTransactionsList.layoutManager as LinearLayoutManager
val scrollState = lm.onSaveInstanceState()
concatAdapter.removeAdapter(cacheAdapter)
concatAdapter.addAdapter(liveAdapter)
lm.onRestoreInstanceState(scrollState)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cacheHasItems should be tied to adapter attachment, not just currentList.

After cacheAdapter is removed, currentList is still non-empty. That keeps cacheHasItems=true, which can hide valid empty/loading states for the live list.

🧩 Suggested fix
-                    val cacheHasItems = cacheAdapter.currentList.isNotEmpty()
+                    val cacheAttached = concatAdapter.adapters.contains(cacheAdapter)
+                    val cacheHasItems = cacheAttached && cacheAdapter.currentList.isNotEmpty()
@@
                     if (loadStates.refresh is LoadState.NotLoading && liveAdapter.itemCount > 0 &&
                         concatAdapter.adapters.contains(cacheAdapter)) {
@@
                         concatAdapter.removeAdapter(cacheAdapter)
+                        cacheAdapter.submitList(emptyList())
                         concatAdapter.addAdapter(liveAdapter)
                         lm.onRestoreInstanceState(scrollState)
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt` around
lines 288 - 305, The problem is that cacheHasItems uses cacheAdapter.currentList
which remains non-empty after cacheAdapter is removed, hiding loading/empty
states; change cacheHasItems to reflect whether the cache adapter is actually
attached to the UI (e.g., check concatAdapter.adapters.contains(cacheAdapter)
and/or cacheAdapter.isAttachedToRecyclerView) and itemCount, e.g., compute
cacheHasItems = (concatAdapter.adapters.contains(cacheAdapter) &&
cacheAdapter.currentList.isNotEmpty()) or use cacheAdapter.itemCount when
attached; update any logic that relies on cacheHasItems (isLoading/isEmpty) so
it only considers cache content when the cacheAdapter is currently part of
concatAdapter.

Copy link
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: 3

♻️ Duplicate comments (2)
wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt (1)

238-247: ⚠️ Potential issue | 🟠 Major

Do not let cacheAdapter.currentList outlive the cache phase.

filterNot { it.isEmpty() } means submitList(emptyList()) never runs, so cacheAdapter.currentList stays populated after TxDataSource.Empty or after the adapter is removed. That keeps cacheHasItems true even when the cache adapter is off-screen, which hides real loading/empty states and can leave stale cache rows visible during a force rebuild. Submit empty lists too, and base cacheHasItems on adapter attachment + item count.

Suggested change
         viewLifecycleOwner.lifecycleScope.launch {
             viewModel.cachedRows
-                .filterNot { it.isEmpty() }
                 .collect { rows ->
                     log.info("STARTUP cache submitList: {} rows at {}", rows.size, System.currentTimeMillis())
                     if (firstPageLoadStartTime == 0L) {
                         firstPageLoadStartTime = System.currentTimeMillis()
                     }
                     cacheAdapter.submitList(rows)
-                    showTransactionList()
+                    if (rows.isNotEmpty()) {
+                        showTransactionList()
+                    }
                 }
         }
@@
-                    val cacheHasItems = cacheAdapter.currentList.isNotEmpty()
+                    val cacheHasItems =
+                        concatAdapter.adapters.contains(cacheAdapter) && cacheAdapter.itemCount > 0
@@
                         val lm = binding.walletTransactionsList.layoutManager as LinearLayoutManager
                         val scrollState = lm.onSaveInstanceState()
                         concatAdapter.removeAdapter(cacheAdapter)
+                        cacheAdapter.submitList(emptyList())
                         concatAdapter.addAdapter(liveAdapter)
                         lm.onRestoreInstanceState(scrollState)
                     }

Also applies to: 295-313

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt` around
lines 238 - 247, The fragment filters out empty cache emissions so
cacheAdapter.submitList(emptyList()) never runs, letting
cacheAdapter.currentList persist and causing cacheHasItems to be stale; change
the cachedRows collector (the block using viewModel.cachedRows, filterNot {
it.isEmpty() }, and cacheAdapter.submitList(rows)) to submit the empty list as
well (remove or alter the filterNot) and update any logic that computes
cacheHasItems to consider both adapter.isAttachedToRecyclerView (or adapter
attachment state) AND adapter.itemCount > 0; apply the same fix to the other
cachedRows collector block that mirrors this behavior (the later block using
cacheAdapter and cacheHasItems) so TxDataSource.Empty truly clears the cache
display.
wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt (1)

539-554: ⚠️ Potential issue | 🟠 Major

Serialize wallet reset on viewModelWorkerScope.

This reset still mutates wrappedTransactionList/contactsByTxId on viewModelScope, then starts cache deletion in a detached launch(Dispatchers.IO). A reset that lands during updateWrappedListForTransactions() or the metadata/contact upserts can repopulate tables after they've been "cleared". Run the whole block on the worker scope and await the deletes before invalidating the paging source.

Suggested change
         walletData.observeWalletReset()
             .onEach {
                 wrappedTransactionList = emptyList()
                 contactsByTxId = mapOf()
-                viewModelScope.launch(Dispatchers.IO) {
-                    txDisplayCacheDao.deleteAll()
-                    txGroupCacheDao.deleteAll()
-                }
+                txDisplayCacheDao.deleteAll()
+                txGroupCacheDao.deleteAll()
                 walletData.wallet?.let { wallet ->
                     coinJoinWrapperFactory = CoinJoinTxWrapperFactory(walletData.networkParameters, wallet as WalletEx)
                     crowdNodeWrapperFactory = FullCrowdNodeSignUpTxSetFactory(walletData.networkParameters, wallet)
                 }
                 _currentPagingSource.value?.invalidate()
             }
             .catch { e -> log.error("wallet reset flow error", e) }
-            .launchIn(viewModelScope)
+            .launchIn(viewModelWorkerScope)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt` around lines 539 -
554, The wallet reset mutates wrappedTransactionList and contactsByTxId on
viewModelScope then launches detached deletes with launch(Dispatchers.IO), which
can race with updateWrappedListForTransactions and metadata upserts; move the
entire observeWalletReset handler to run inside viewModelWorkerScope, perform
txDisplayCacheDao.deleteAll() and txGroupCacheDao.deleteAll() as suspend calls
(or with withContext(Dispatchers.IO)) and await their completion before
invalidating _currentPagingSource.value, and ensure coinJoinWrapperFactory /
crowdNodeWrapperFactory assignment and the wrappedTransactionList/contactsByTxId
clears occur within that serialized worker-scope block so no repopulation races
occur.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 923-927: The lazy wrapper loading path (loadWrapperOnDemand)
mutates the shared wrappedTransactionList from callers using
lifecycleScope.launch (e.g., WalletTransactionsFragment), creating a race with
the viewModelWorkerScope processing pipeline; ensure the lookup-and-append runs
on the same worker scope or is synchronized: move the wrappedTransactionList
none/{wrappedTransactionList = ...} update into viewModelWorkerScope (use
viewModelWorkerScope.launch { ... }) or protect wrappedTransactionList with a
mutex/state holder (e.g., a Mutex around the check+append or an
Atomic/MutableState wrapper) so loadWrapperOnDemand and the worker pipeline
cannot interleave and lose updates.
- Around line 387-409: The startup path loads all rows via
txDisplayCacheDao.getAll() and promotes them to TxDataSource.PrebuiltCache
without applying the restored transactionsDirection filter, which can show the
wrong tab; modify the coroutine that builds historyRows (use
transactionsDirection/current direction variable restored around Line 380) to
filter cachedRows before mapping (e.g., only include entries whose direction
matches the current transactionsDirection or map to TransactionRowView then
filter by its direction) so that when you assign _txDataSource.value =
TxDataSource.PrebuiltCache(historyRows) and set _transactionsLoaded.value = true
the PrebuiltCache only contains rows matching the active direction; use
contactsByTxId and existing toTransactionRowView conversion but apply the
direction check prior to adding to historyRows.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt`:
- Around line 324-332: The loading overlay is driven from two places which
conflicts: the inline assignment using isLoading ||
viewModel.isBuildingCache.value and the later observer on
viewModel.isBuildingCache that overwrites it; remove the second observer and
drive binding.loading.isVisible from a single combined state instead (e.g.
create a combined Boolean in the fragment or a MediatorLiveData/LiveData in the
ViewModel like isLoadingCombined that merges isLoading and
viewModel.isBuildingCache, then observe that single source and set
binding.loading.isVisible accordingly), ensuring references to
binding.loading.isVisible, viewModel.isBuildingCache, the local isLoading flag,
and the existing showEmptyView()/showTransactionList() logic remain consistent.

---

Duplicate comments:
In `@wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt`:
- Around line 539-554: The wallet reset mutates wrappedTransactionList and
contactsByTxId on viewModelScope then launches detached deletes with
launch(Dispatchers.IO), which can race with updateWrappedListForTransactions and
metadata upserts; move the entire observeWalletReset handler to run inside
viewModelWorkerScope, perform txDisplayCacheDao.deleteAll() and
txGroupCacheDao.deleteAll() as suspend calls (or with
withContext(Dispatchers.IO)) and await their completion before invalidating
_currentPagingSource.value, and ensure coinJoinWrapperFactory /
crowdNodeWrapperFactory assignment and the wrappedTransactionList/contactsByTxId
clears occur within that serialized worker-scope block so no repopulation races
occur.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt`:
- Around line 238-247: The fragment filters out empty cache emissions so
cacheAdapter.submitList(emptyList()) never runs, letting
cacheAdapter.currentList persist and causing cacheHasItems to be stale; change
the cachedRows collector (the block using viewModel.cachedRows, filterNot {
it.isEmpty() }, and cacheAdapter.submitList(rows)) to submit the empty list as
well (remove or alter the filterNot) and update any logic that computes
cacheHasItems to consider both adapter.isAttachedToRecyclerView (or adapter
attachment state) AND adapter.itemCount > 0; apply the same fix to the other
cachedRows collector block that mirrors this behavior (the later block using
cacheAdapter and cacheHasItems) so TxDataSource.Empty truly clears the cache
display.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68b9f235-ae6b-4b32-aa90-3cb8d38038a2

📥 Commits

Reviewing files that changed from the base of the PR and between acda65c and 3278855.

📒 Files selected for processing (2)
  • wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt

Comment on lines +324 to +332
binding.loading.isVisible = isLoading || viewModel.isBuildingCache.value
if (isEmpty && header.isEmpty()) showEmptyView() else showTransactionList()
}
}

// Show the "determining transaction history" overlay while the cache is being built.
viewModel.isBuildingCache.observe(viewLifecycleOwner) { building ->
binding.loading.isVisible = building
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use one source of truth for the loading overlay.

Lines 324-325 already compute isLoading || viewModel.isBuildingCache.value, but the separate observer on Lines 329-332 later overwrites that with just building. When a rebuild flips building to false before the first live page arrives, this second collector can hide the spinner while the list is still empty and loading. Keep binding.loading.isVisible driven from a single combined state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt` around
lines 324 - 332, The loading overlay is driven from two places which conflicts:
the inline assignment using isLoading || viewModel.isBuildingCache.value and the
later observer on viewModel.isBuildingCache that overwrites it; remove the
second observer and drive binding.loading.isVisible from a single combined state
instead (e.g. create a combined Boolean in the fragment or a
MediatorLiveData/LiveData in the ViewModel like isLoadingCombined that merges
isLoading and viewModel.isBuildingCache, then observe that single source and set
binding.loading.isVisible accordingly), ensuring references to
binding.loading.isVisible, viewModel.isBuildingCache, the local isLoading flag,
and the existing showEmptyView()/showTransactionList() logic remain consistent.

Copy link
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: 3

♻️ Duplicate comments (2)
wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt (2)

326-344: ⚠️ Potential issue | 🟡 Minor

Loading overlay is controlled from two places.

binding.loading.isVisible is set both in the loadStateFlow collector (line 328) and in the isBuildingCache observer (line 339). These can conflict: the isBuildingCache observer runs independently and may overwrite the computed value from the load state collector.

Consider combining these into a single source of truth for the loading state.

🔧 Suggested approach: single combined observer

Remove the separate isBuildingCache.observe and incorporate its logic into the loadStateFlow collector where buildingFromScratch is already computed. This ensures one code path controls the loading visibility:

// In loadStateFlow collector (already has buildingFromScratch):
binding.loading.isVisible = isLoading || buildingFromScratch

// Handle the toast separately:
viewModel.isBuildingCache.observe(viewLifecycleOwner) { building ->
    if (!building && pendingManualRefresh) {
        pendingManualRefresh = false
        Toast.makeText(requireContext(), R.string.history_refresh_complete, Toast.LENGTH_SHORT).show()
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt` around
lines 326 - 344, The loading overlay is being set in two places causing
conflicts; remove the separate viewModel.isBuildingCache.observe block that
assigns binding.loading.isVisible and instead compute visibility only inside the
loadStateFlow collector using the existing buildingFromScratch and isLoading
values (keep binding.loading.isVisible = isLoading || buildingFromScratch
there), then preserve a minimal observer for viewModel.isBuildingCache only to
handle the pendingManualRefresh/Toast logic (when !building &&
pendingManualRefresh reset flag and show history_refresh_complete). Ensure you
reference the same symbols: loadStateFlow collector, buildingFromScratch,
binding.loading.isVisible, viewModel.isBuildingCache.observe,
pendingManualRefresh, and the Toast call.

296-302: ⚠️ Potential issue | 🟡 Minor

cacheHasItems may still produce incorrect state after adapter swap.

After cacheAdapter is removed from concatAdapter (line 313), cacheAdapter.currentList remains non-empty because submitList(emptyList()) is never called. This means cacheHasItems can incorrectly remain true, potentially hiding valid empty states.

🔧 Suggested fix: clear cache list after swap
                     if (loadStates.refresh is LoadState.NotLoading && liveAdapter.itemCount > 0 &&
                         concatAdapter.adapters.contains(cacheAdapter)) {
                         log.info("STARTUP swapping to live adapter: {} items at {}",
                             liveAdapter.itemCount, System.currentTimeMillis())
                         val lm = binding.walletTransactionsList.layoutManager as LinearLayoutManager
                         val scrollState = lm.onSaveInstanceState()
                         concatAdapter.removeAdapter(cacheAdapter)
+                        cacheAdapter.submitList(emptyList())  // Clear to fix cacheHasItems check
                         concatAdapter.addAdapter(liveAdapter)
                         lm.onRestoreInstanceState(scrollState)
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt` around
lines 296 - 302, The cacheAdapter's currentList can remain non-empty after you
remove it from the concat adapter, causing cacheHasItems to be stale; after the
code path that removes or swaps out cacheAdapter from concatAdapter (where
concatAdapter.removeAdapter(cacheAdapter) or the adapter swap happens),
explicitly clear its data by calling cacheAdapter.submitList(emptyList()) so
cacheAdapter.currentList becomes empty and isEmpty/isLoading are computed
correctly.
🧹 Nitpick comments (1)
wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt (1)

678-695: Parallel contact resolution may overwhelm the platform.

resolveAllContacts launches parallel async coroutines on Dispatchers.IO for every transaction needing contact resolution (line 681). With a large transaction history, this could spawn hundreds of concurrent network requests to the platform.

Consider limiting concurrency or batching requests.

♻️ Suggested approach using chunking
// Process in batches of 10 to limit concurrent platform calls
val resolved = coroutineScope {
    txsToResolve
        .chunked(10)
        .flatMap { chunk ->
            chunk.map { tx ->
                async(Dispatchers.IO) {
                    // ... existing resolution logic
                }
            }.awaitAll()
        }
        .filterNotNull()
        .toMap()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt` around
lines 678 - 695, The current implementation launches an async(Dispatchers.IO)
for every element in txsToResolve which can spawn too many concurrent platform
calls; modify resolveAllContacts to limit concurrency by processing txsToResolve
in bounded batches (e.g., chunked(10)) or by using a
CoroutineDispatcher/Semaphore to cap parallelism, then for each batch map to
async { platformRepo.blockchainIdentity.getContactForTransaction(tx) ... }
.awaitAll(), collect non-null pairs and merge to a map as before; reference
txsToResolve, platformRepo.blockchainIdentity.getContactForTransaction, and
Dispatchers.IO when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt`:
- Around line 314-329: The nested serviceScope.launch causes cache deletions to
run asynchronously and can race with subsequent resets; remove the inner launch
and call txDisplayCacheDao.deleteAll() and txGroupCacheDao.deleteAll() directly
(they are suspend functions) inside the onEach block so deletions complete
before resetting wrappedTransactionList, setting
coinJoinWrapperFactory/crowdNodeWrapperFactory, and calling
_currentPagingSource.value?.invalidate(); this ensures synchronous cache
clearing when walletData.observeWalletReset() fires.
- Around line 212-234: The force-unwrap walletData.wallet!! inside the
flatMapLatest (where TxDirectionFilter(direction, walletData.wallet!!) is
created) can throw and terminate the flow; change to a null-safe pattern: inside
the flatMapLatest retrieve walletData.wallet into a local val (e.g., val w =
walletData.wallet) and if it's null return an empty/idle flow (or skip by using
return@flatMapLatest emptyFlow()) so the branch uses a non-null wallet only when
available; update subsequent calls that reference the wallet (TxDirectionFilter
and walletData.observeTransactions) to use that local non-null wallet and avoid
the !! operator so the flow remains alive during shutdown/initialization.
- Around line 368-377: The forceRebuildTransactionCache function calls
walletData.wallet!! which can NPE; change it to defensively handle a null wallet
by checking walletData.wallet first and returning early (or setting
TxDataSource.Empty) if null, then only create the TxDirectionFilter and call
rebuildWrappedList when wallet is non-null; locate this in
forceRebuildTransactionCache and replace the force-unwrap usage with a safe
check (e.g., let/if-null-return) before constructing TxDirectionFilter and
invoking rebuildWrappedList.

---

Duplicate comments:
In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt`:
- Around line 326-344: The loading overlay is being set in two places causing
conflicts; remove the separate viewModel.isBuildingCache.observe block that
assigns binding.loading.isVisible and instead compute visibility only inside the
loadStateFlow collector using the existing buildingFromScratch and isLoading
values (keep binding.loading.isVisible = isLoading || buildingFromScratch
there), then preserve a minimal observer for viewModel.isBuildingCache only to
handle the pendingManualRefresh/Toast logic (when !building &&
pendingManualRefresh reset flag and show history_refresh_complete). Ensure you
reference the same symbols: loadStateFlow collector, buildingFromScratch,
binding.loading.isVisible, viewModel.isBuildingCache.observe,
pendingManualRefresh, and the Toast call.
- Around line 296-302: The cacheAdapter's currentList can remain non-empty after
you remove it from the concat adapter, causing cacheHasItems to be stale; after
the code path that removes or swaps out cacheAdapter from concatAdapter (where
concatAdapter.removeAdapter(cacheAdapter) or the adapter swap happens),
explicitly clear its data by calling cacheAdapter.submitList(emptyList()) so
cacheAdapter.currentList becomes empty and isEmpty/isLoading are computed
correctly.

---

Nitpick comments:
In `@wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt`:
- Around line 678-695: The current implementation launches an
async(Dispatchers.IO) for every element in txsToResolve which can spawn too many
concurrent platform calls; modify resolveAllContacts to limit concurrency by
processing txsToResolve in bounded batches (e.g., chunked(10)) or by using a
CoroutineDispatcher/Semaphore to cap parallelism, then for each batch map to
async { platformRepo.blockchainIdentity.getContactForTransaction(tx) ... }
.awaitAll(), collect non-null pairs and merge to a map as before; reference
txsToResolve, platformRepo.blockchainIdentity.getContactForTransaction, and
Dispatchers.IO when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ea7c356-4991-49e9-b030-c89f5a882099

📥 Commits

Reviewing files that changed from the base of the PR and between 3278855 and ad1726a.

📒 Files selected for processing (5)
  • wallet/res/values/strings-extra.xml
  • wallet/src/de/schildbach/wallet/WalletApplication.java
  • wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt
  • wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt
  • wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • wallet/res/values/strings-extra.xml
  • wallet/src/de/schildbach/wallet/WalletApplication.java

Comment on lines +314 to +329
walletData.observeWalletReset()
.onEach {
wrappedTransactionList = emptyList()
contactsByTxId = mapOf()
serviceScope.launch {
txDisplayCacheDao.deleteAll()
txGroupCacheDao.deleteAll()
}
walletData.wallet?.let { wallet ->
coinJoinWrapperFactory = CoinJoinTxWrapperFactory(walletData.networkParameters, wallet as WalletEx)
crowdNodeWrapperFactory = FullCrowdNodeSignUpTxSetFactory(walletData.networkParameters, wallet)
}
_currentPagingSource.value?.invalidate()
}
.catch { e -> log.error("wallet reset flow error", e) }
.launchIn(serviceScope)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Nested coroutine launch during wallet reset may cause race condition.

At line 318, a nested serviceScope.launch is used for cache deletion. Since the outer onEach already runs on serviceScope, the deletion happens asynchronously while the wrapper factory reset (lines 322-325) and paging source invalidation (line 326) proceed immediately.

If a transaction update arrives during deletion, it could write to a cache being cleared. Consider using synchronous execution.

🔧 Suggested fix: make deletion synchronous within the flow
         walletData.observeWalletReset()
             .onEach {
                 wrappedTransactionList = emptyList()
                 contactsByTxId = mapOf()
-                serviceScope.launch {
-                    txDisplayCacheDao.deleteAll()
-                    txGroupCacheDao.deleteAll()
-                }
+                // Delete synchronously to prevent races with incoming tx updates
+                txDisplayCacheDao.deleteAll()
+                txGroupCacheDao.deleteAll()
                 walletData.wallet?.let { wallet ->
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/service/TxDisplayCacheService.kt` around
lines 314 - 329, The nested serviceScope.launch causes cache deletions to run
asynchronously and can race with subsequent resets; remove the inner launch and
call txDisplayCacheDao.deleteAll() and txGroupCacheDao.deleteAll() directly
(they are suspend functions) inside the onEach block so deletions complete
before resetting wrappedTransactionList, setting
coinJoinWrapperFactory/crowdNodeWrapperFactory, and calling
_currentPagingSource.value?.invalidate(); this ensures synchronous cache
clearing when walletData.observeWalletReset() fires.

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.

1 participant