Skip to content

Improve null handling with backend data#214

Open
caleb-bit wants to merge 14 commits intomainfrom
Caleb/transaction-fix
Open

Improve null handling with backend data#214
caleb-bit wants to merge 14 commits intomainfrom
Caleb/transaction-fix

Conversation

@caleb-bit
Copy link
Contributor

@caleb-bit caleb-bit commented Mar 18, 2026

Overview

This PR prevents app crashes by making response data fields nullable and handling null data appropriately in the UI.

Changes Made

  • Make response fields nullable
  • Remove deprecated TransactionType
  • Use UI friendly DisplayTransaction data structure which avoids nullable fields

Next Steps

Summary by CodeRabbit

  • Refactor

    • Improved transaction and account null-safety and parsing; consolidated UI state shapes across screens for more consistent behavior
    • Simplified account/session flows for more reliable sign-in state
  • Bug Fixes

    • Prevents malformed or incomplete transactions from appearing in lists
    • Favorites extraction now safely ignores missing items
  • UI

    • Unified transaction display model and stable list keys for consistent rendering; simplified amount/date formatting
  • Performance

    • Per-item caching for faster navigation from recent searches to details

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 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

Removed TransactionType and its Moshi adapter; introduced DisplayTransaction UI model; reshaped transaction models and nullability; added GetAccountRepository and repository network helpers; refactored token/session flows and updated viewmodels/composables to use DisplayTransaction.

Changes

Cohort / File(s) Summary
Serialization & Networking
app/src/main/java/com/cornellappdev/android/eatery/data/MoshiAdapters.kt, app/src/main/java/com/cornellappdev/android/eatery/di/NetworkingModule.kt
Removed TransactionType enum usage and deleted its Moshi adapter and registration.
Data Models
app/src/main/java/com/cornellappdev/android/eatery/data/models/User.kt
Changed nested list element nullability; altered User/Account/Financials nullability/defaults; added TransactionResponse (nullable payload); replaced Transaction structure (strongly-typed fields, LocalDateTime), and removed TransactionType.
Repositories & Network Utilities
app/src/main/java/com/cornellappdev/android/eatery/data/repositories/...
Added GetAccountRepository (new session/linking APIs); removed session/linking APIs from AuthTokenRepository; added RepositoryRequestUtils.kt with safeNetworkRequest and tryRequestWithTokenRefresh and exception→NetworkError mapping; updated UserRepository to use getAccountRepository, null-safe transaction parsing, String.toLocalDateTime() helper, and simplified retry/token-refresh wiring.
ViewModels & State Models
app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/..., app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/state/DisplayTransaction.kt
Added DisplayTransaction; removed TransactionWithFormattedDate; consolidated many flows into uiState: StateFlow<…> patterns (LoginViewModel, SearchViewModel, HomeViewModel, FavoritesViewModel, NearestViewModel); introduced state caching (observeEatery) and stateIn(..., SharingStarted.WhileSubscribed(5_000)).
UI / Composables
app/src/main/java/com/cornellappdev/android/eatery/ui/components/login/AccountPage.kt, app/src/main/java/com/cornellappdev/android/eatery/ui/screens/ProfileScreen.kt, app/src/main/java/com/cornellappdev/android/eatery/ui/screens/SearchScreen.kt, app/src/main/java/com/cornellappdev/android/eatery/ui/screens/*
Updated composable signatures to accept List<DisplayTransaction>; refactored TransactionRow to accept DisplayTransaction; replaced usages of openEatery with observeEatery; simplified UI consumers to read from consolidated uiState.
Minor ViewModel Adjustments
app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/EateryDetailViewModel.kt, .../SearchViewModel.kt
Converted some Flows to StateFlows with explicit initial Pending values and adjusted imports/combines to consume these StateFlows.

Sequence Diagram

sequenceDiagram
    participant UI as UI Layer
    participant VM as ViewModel
    participant Repo as UserRepository
    participant GET as GetAccountRepository
    participant Auth as AuthTokenRepository
    participant Net as NetworkApi

    UI->>VM: subscribe uiState / request transactions
    VM->>Repo: fetch user & financials
    Repo->>GET: getSessionId / isLoggedIn (prefs)
    Repo->>Auth: tryRequestWithTokenRefresh(..., refreshTokens)
    Auth->>Net: refreshTokens() (if needed)
    Net-->>Auth: tokens
    Auth-->>Repo: refresh result
    Repo->>Net: fetch financials (network call)
    Net-->>Repo: financials payload (nullable)
    Repo->>Repo: filter, parse dates (String.toLocalDateTime), map to Transaction -> DisplayTransaction
    Repo-->>VM: validated DisplayTransaction list
    VM-->>UI: emit uiState with DisplayTransaction list
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hopped through adapters, nibbled types anew,
I stitched nulls and tokens, parsed timestamps true,
I birthed DisplayTransaction for the UI view,
A tiny rabbit fix — concise, robust, and few,
Hop, hop — the flows now pass, all snug like dew. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.57% 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
Title check ✅ Passed The title clearly summarizes the main objective: improving null handling with backend data, which aligns with the primary changes removing null-safety assumptions in response models.
Description check ✅ Passed The description covers the key sections but lacks detail in the Test Coverage section (only has TODO) and provides minimal specifics about changes.

✏️ 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 Caleb/transaction-fix
📝 Coding Plan
  • Generate coding plan for human review comments

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/components/login/AccountPage.kt`:
- Around line 435-443: The current fallback always formats non-zero amounts as
negative red; change the logic in the amtString/amtColor assignment to use the
numeric sign of amount (abs only for magnitude) so positives render as "+$%.2f"
with Green and negatives as "-$%.2f" with Red, while preserving the meal-swipes
branch (isMealSwipes) and the zero case (amount.epsilonEqual(0.0)); also
reintroduce the Green color import if you use the sign-based fallback.

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt`:
- Around line 136-142: Replace the composite key currently passed to
DisplayTransaction.id with the backend transaction's tenderId: when building
DisplayTransaction (the block that constructs id from date, location, amount,
mappedAccountType), use transaction.tenderId as the primary id; if tenderId is
null/empty, produce a deterministic unique fallback by using mapIndexedNotNull
(or mapIndexed) to incorporate the source index into the id so each item in the
snapshot is unique and prevents LazyColumn key collisions. Ensure references to
DisplayTransaction and the Transaction model (tenderId) are updated accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab742e90-7778-4d43-b41c-dba6a1363c07

📥 Commits

Reviewing files that changed from the base of the PR and between ae30974 and d4901b8.

📒 Files selected for processing (8)
  • app/src/main/java/com/cornellappdev/android/eatery/data/MoshiAdapters.kt
  • app/src/main/java/com/cornellappdev/android/eatery/data/models/User.kt
  • app/src/main/java/com/cornellappdev/android/eatery/data/repositories/UserRepository.kt
  • app/src/main/java/com/cornellappdev/android/eatery/di/NetworkingModule.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/components/login/AccountPage.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/screens/ProfileScreen.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/state/DisplayTransaction.kt
💤 Files with no reviewable changes (2)
  • app/src/main/java/com/cornellappdev/android/eatery/di/NetworkingModule.kt
  • app/src/main/java/com/cornellappdev/android/eatery/data/MoshiAdapters.kt

Copy link
Member

@AndrewCheung360 AndrewCheung360 left a comment

Choose a reason for hiding this comment

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

Left some minor comments to look over, but as long as it's been tested, I'll just give approval preemptively

Copy link

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

🧹 Nitpick comments (2)
app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt (2)

116-120: Consider precomputing repeated values.

query.lowercase() and LocalDateTime.now().minusDays(30) are evaluated for each transaction. For larger lists, extracting these to local variables avoids redundant allocations and computation.

♻️ Optional optimization
         ) { loadedUser, query, accountFilter ->
             if (loadedUser == null) return@combine emptyList()
+            val lowerQuery = query.lowercase()
+            val cutoff = LocalDateTime.now().minusDays(30)
             loadedUser.transactions.filter {
-                it.location.lowercase().contains(query.lowercase())
-                        && it.accountType == accountFilter
-                        && it.date >= LocalDateTime.now().minusDays(30)
+                it.location.lowercase().contains(lowerQuery)
+                        && it.accountType == accountFilter
+                        && it.date >= cutoff
             }.map { it.toDisplayTransaction() }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt`
around lines 116 - 120, The filtering currently calls query.lowercase() and
LocalDateTime.now().minusDays(30) for every transaction inside
loadedUser.transactions.filter; refactor the block to compute val lowerQuery =
query.lowercase() (or use locale if needed) and val cutoff =
LocalDateTime.now().minusDays(30) once before calling
loadedUser.transactions.filter, then use lowerQuery and cutoff inside the
predicate (preserve accountFilter and the toDisplayTransaction() mapping).

124-127: Consider locale-aware date formatting for internationalization.

The hardcoded pattern "h:mm a · EEEE, MMMM d" assumes US locale conventions. If the app supports multiple locales, consider using DateTimeFormatter.ofLocalizedDateTime() or passing a Locale parameter.

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

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt`
around lines 124 - 127, The LocalDateTime.formatDate() helper currently uses a
hardcoded pattern ("h:mm a · EEEE, MMMM d") which is not locale-aware; update
formatDate() to use a locale-sensitive formatter (e.g.,
DateTimeFormatter.ofLocalizedDateTime(...) and call
localizedBy(Locale.getDefault()) or accept a Locale parameter) instead of
DateTimeFormatter.ofPattern, so displayed dates follow the device/app locale;
modify the LocalDateTime.formatDate() implementation accordingly (or add an
overload that takes a Locale) to ensure internationalized formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt`:
- Around line 116-120: The filtering currently calls query.lowercase() and
LocalDateTime.now().minusDays(30) for every transaction inside
loadedUser.transactions.filter; refactor the block to compute val lowerQuery =
query.lowercase() (or use locale if needed) and val cutoff =
LocalDateTime.now().minusDays(30) once before calling
loadedUser.transactions.filter, then use lowerQuery and cutoff inside the
predicate (preserve accountFilter and the toDisplayTransaction() mapping).
- Around line 124-127: The LocalDateTime.formatDate() helper currently uses a
hardcoded pattern ("h:mm a · EEEE, MMMM d") which is not locale-aware; update
formatDate() to use a locale-sensitive formatter (e.g.,
DateTimeFormatter.ofLocalizedDateTime(...) and call
localizedBy(Locale.getDefault()) or accept a Locale parameter) instead of
DateTimeFormatter.ofPattern, so displayed dates follow the device/app locale;
modify the LocalDateTime.formatDate() implementation accordingly (or add an
overload that takes a Locale) to ensure internationalized formatting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a6ed460f-7613-47b5-9be0-faca3087fc8e

📥 Commits

Reviewing files that changed from the base of the PR and between d4901b8 and 48dc540.

📒 Files selected for processing (5)
  • app/src/main/java/com/cornellappdev/android/eatery/data/models/User.kt
  • app/src/main/java/com/cornellappdev/android/eatery/data/repositories/UserRepository.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/components/login/AccountPage.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/state/DisplayTransaction.kt
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/state/DisplayTransaction.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/cornellappdev/android/eatery/data/repositories/UserRepository.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/components/login/AccountPage.kt

Copy link

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

🧹 Nitpick comments (1)
app/src/main/java/com/cornellappdev/android/eatery/data/repositories/GetAccountRepository.kt (1)

44-46: Silent default values may mask missing session state.

Returning "" for missing session ID and 0 for missing PIN makes it impossible for callers to distinguish between "not set" and "set to empty/zero." Consider returning nullable types or throwing if these are required preconditions.

💡 Alternative approach
-    suspend fun getSessionId(): String = userPreferencesRepository.sessionIdFlow.firstOrNull() ?: ""
+    suspend fun getSessionId(): String? = userPreferencesRepository.sessionIdFlow.firstOrNull()

-    suspend fun getPin(): Int = userPreferencesRepository.pinFlow.firstOrNull() ?: 0
+    suspend fun getPin(): Int? = userPreferencesRepository.pinFlow.firstOrNull()

Or throw if these are preconditions:

suspend fun getSessionId(): String = userPreferencesRepository.sessionIdFlow.firstOrNull()
    ?: throw IllegalStateException("Session ID not available")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/android/eatery/data/repositories/GetAccountRepository.kt`
around lines 44 - 46, The current getSessionId() and getPin() functions return
silent defaults ("" and 0) which mask missing state; update getSessionId and
getPin in GetAccountRepository to surface absence by either changing their
return types to nullable (String? and Int?) and returning
userPreferencesRepository.sessionIdFlow.firstOrNull() / pinFlow.firstOrNull(),
or keep non-nullable and throw an explicit exception (e.g.,
IllegalStateException) when firstOrNull() is null; update all callers of
getSessionId and getPin to handle the nullable return or catch/propagate the
thrown exception accordingly so missing session/PIN is no longer
indistinguishable from empty/zero.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/cornellappdev/android/eatery/data/repositories/RepositoryRequestUtils.kt`:
- Around line 17-36: Capture the original exception thrown by request() and map
it to a network error first (e.g., call mapExceptionToNetworkError on the caught
exception) and only attempt refreshTokens() if that mapped error indicates an
authentication problem; if it is not an auth error, return Result.Error<T> with
the mapped original error immediately. If you do call refreshTokens() and it
returns Result.Error<Unit>, convert/propagate that error as a Result.Error<T>
(do not return the Result<Unit> directly) so the function consistently returns
Result<T>. Keep the existing retry block that maps retryException via
mapExceptionToNetworkError and returns Result.Error<T> on retry failure; update
tryRequestWithTokenRefresh, request, refreshTokens, and the places where you
check the mapped error to implement this flow.

---

Nitpick comments:
In
`@app/src/main/java/com/cornellappdev/android/eatery/data/repositories/GetAccountRepository.kt`:
- Around line 44-46: The current getSessionId() and getPin() functions return
silent defaults ("" and 0) which mask missing state; update getSessionId and
getPin in GetAccountRepository to surface absence by either changing their
return types to nullable (String? and Int?) and returning
userPreferencesRepository.sessionIdFlow.firstOrNull() / pinFlow.firstOrNull(),
or keep non-nullable and throw an explicit exception (e.g.,
IllegalStateException) when firstOrNull() is null; update all callers of
getSessionId and getPin to handle the nullable return or catch/propagate the
thrown exception accordingly so missing session/PIN is no longer
indistinguishable from empty/zero.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 581e3274-070e-4fc0-a6ed-97f8a57df05a

📥 Commits

Reviewing files that changed from the base of the PR and between 48dc540 and 1b4a5c5.

📒 Files selected for processing (5)
  • app/src/main/java/com/cornellappdev/android/eatery/data/repositories/AuthTokenRepository.kt
  • app/src/main/java/com/cornellappdev/android/eatery/data/repositories/GetAccountRepository.kt
  • app/src/main/java/com/cornellappdev/android/eatery/data/repositories/RepositoryRequestUtils.kt
  • app/src/main/java/com/cornellappdev/android/eatery/data/repositories/UserRepository.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/cornellappdev/android/eatery/data/repositories/UserRepository.kt

Copy link

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

🧹 Nitpick comments (3)
app/src/main/java/com/cornellappdev/android/eatery/ui/screens/SearchScreen.kt (1)

259-261: Redundant initialValue parameter.

Since observeEatery returns a StateFlow which already has an initial value of EateryApiResponse.Pending, specifying initialValue in collectAsStateWithLifecycle is redundant. The overload without initialValue can be used for StateFlow.

♻️ Suggested simplification
 val eateryResponse =
-    searchViewModel.observeEatery(eateryId).collectAsStateWithLifecycle(
-        initialValue = EateryApiResponse.Pending
-    ).value
+    searchViewModel.observeEatery(eateryId).collectAsStateWithLifecycle().value
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/screens/SearchScreen.kt`
around lines 259 - 261, The call to collectAsStateWithLifecycle passes a
redundant initialValue because searchViewModel.observeEatery(eateryId) already
returns a StateFlow seeded with EateryApiResponse.Pending; remove the
initialValue = EateryApiResponse.Pending argument from the
collectAsStateWithLifecycle invocation so it uses the StateFlow's own initial
value (locate the expression using observeEatery and collectAsStateWithLifecycle
in SearchScreen.kt and delete the initialValue parameter).
app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/SearchViewModel.kt (1)

33-34: Consider thread-safety and memory growth for eateryFlowCache.

The mutableMapOf is not thread-safe. While getOrPut is typically called from the main thread via composables, concurrent calls could lead to duplicate StateFlow creations. Additionally, the cache grows indefinitely without eviction.

Consider using a ConcurrentHashMap or synchronizing access. For memory, the WhileSubscribed(5_000) policy helps, but stale entries remain in the map.

♻️ Suggested improvement using ConcurrentHashMap
+import java.util.concurrent.ConcurrentHashMap
+
 `@HiltViewModel`
 class SearchViewModel `@Inject` constructor(
     private val userPreferencesRepository: UserPreferencesRepository,
     private val eateryRepository: EateryRepository,
     private val userRepository: UserRepository
 ) : ViewModel() {
-    private val eateryFlowCache = mutableMapOf<Int, StateFlow<EateryApiResponse<Eatery>>>()
+    private val eateryFlowCache = ConcurrentHashMap<Int, StateFlow<EateryApiResponse<Eatery>>>()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/SearchViewModel.kt`
around lines 33 - 34, Replace the non-thread-safe mutableMapOf eateryFlowCache
with a ConcurrentHashMap<Int, StateFlow<EateryApiResponse<Eatery>>> and switch
usages that create flows (currently using getOrPut) to computeIfAbsent so
duplicate StateFlow creation is avoided under concurrency; additionally
implement eviction: when creating the StateFlow (the one using
WhileSubscribed(5_000)), observe its subscriptionCount in a coroutine and remove
its entry from the ConcurrentHashMap after subscriptionCount drops to zero for
the desired idle period (or use a simple LRU/size-bound policy) so stale entries
don't grow unbounded. Ensure references to eateryFlowCache and the flow-creation
function (the code path that currently calls getOrPut) are updated accordingly.
app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt (1)

106-112: Consider precomputing filter values for slight efficiency gain.

LocalDateTime.now().minusDays(30) and query.lowercase() are evaluated for each transaction during filtering. For large lists, extracting these before the filter would avoid repeated allocations.

♻️ Suggested optimization
     ) { loadedUser, query, accountFilter ->
         if (loadedUser == null) return@combine emptyList()
+        val cutoffDate = LocalDateTime.now().minusDays(30)
+        val lowerQuery = query.lowercase()
         loadedUser.transactions.filter {
-            it.location.lowercase().contains(query.lowercase())
+            it.location.lowercase().contains(lowerQuery)
                     && it.accountType == accountFilter
-                    && it.date >= LocalDateTime.now().minusDays(30)
+                    && it.date >= cutoffDate
         }.map { it.toDisplayTransaction() }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt`
around lines 106 - 112, The filter repeatedly computes query.lowercase() and
LocalDateTime.now().minusDays(30) for every transaction; precompute these once
before filtering in the combine lambda inside LoginViewModel (the block that
takes loadedUser, query, accountFilter) — e.g., assign val lowerQuery =
query.lowercase() and val cutoff = LocalDateTime.now().minusDays(30) and then
use lowerQuery and cutoff inside loadedUser.transactions.filter { ... } so the
expensive/allocating calls are not executed per-transaction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/screens/SearchScreen.kt`:
- Around line 259-261: The call to collectAsStateWithLifecycle passes a
redundant initialValue because searchViewModel.observeEatery(eateryId) already
returns a StateFlow seeded with EateryApiResponse.Pending; remove the
initialValue = EateryApiResponse.Pending argument from the
collectAsStateWithLifecycle invocation so it uses the StateFlow's own initial
value (locate the expression using observeEatery and collectAsStateWithLifecycle
in SearchScreen.kt and delete the initialValue parameter).

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt`:
- Around line 106-112: The filter repeatedly computes query.lowercase() and
LocalDateTime.now().minusDays(30) for every transaction; precompute these once
before filtering in the combine lambda inside LoginViewModel (the block that
takes loadedUser, query, accountFilter) — e.g., assign val lowerQuery =
query.lowercase() and val cutoff = LocalDateTime.now().minusDays(30) and then
use lowerQuery and cutoff inside loadedUser.transactions.filter { ... } so the
expensive/allocating calls are not executed per-transaction.

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/SearchViewModel.kt`:
- Around line 33-34: Replace the non-thread-safe mutableMapOf eateryFlowCache
with a ConcurrentHashMap<Int, StateFlow<EateryApiResponse<Eatery>>> and switch
usages that create flows (currently using getOrPut) to computeIfAbsent so
duplicate StateFlow creation is avoided under concurrency; additionally
implement eviction: when creating the StateFlow (the one using
WhileSubscribed(5_000)), observe its subscriptionCount in a coroutine and remove
its entry from the ConcurrentHashMap after subscriptionCount drops to zero for
the desired idle period (or use a simple LRU/size-bound policy) so stale entries
don't grow unbounded. Ensure references to eateryFlowCache and the flow-creation
function (the code path that currently calls getOrPut) are updated accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8d590c00-5ba6-4e18-81f7-0e26bb3a971b

📥 Commits

Reviewing files that changed from the base of the PR and between 1b4a5c5 and c5b2382.

📒 Files selected for processing (4)
  • app/src/main/java/com/cornellappdev/android/eatery/ui/screens/SearchScreen.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/EateryDetailViewModel.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/SearchViewModel.kt

Copy link

@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 (4)
app/src/main/java/com/cornellappdev/android/eatery/ui/screens/NearestScreen.kt (2)

104-108: ⚠️ Potential issue | 🟡 Minor

Potential NPE with force-unwrapped eatery.id.

Same issue as in FavoritesScreen - using eatery.id!! as a key can crash if an eatery has a null ID.

🛡️ Suggested safer key
 items(
     items = nearestEateries,
     key = { eatery ->
-        eatery.id!!
+        eatery.id ?: eatery.hashCode()
     }) { eatery ->
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/screens/NearestScreen.kt`
around lines 104 - 108, The key provider for the LazyColumn items uses a
force-unwrapped ID (items(... key = { eatery -> eatery.id!! })) which can throw
an NPE; update the key logic in NearestScreen to use a null-safe fallback (e.g.,
key = { eatery -> eatery.id ?: eatery.hashCode() } or combine a stable field
like name/index) so it never force-unwraps, mirroring the safer approach used in
FavoritesScreen.

88-89: ⚠️ Potential issue | 🟡 Minor

Incorrect empty state message for "Nearest to You" screen.

The message says "You currently have no favorite eateries!" but this is the NearestScreen, which displays eateries sorted by proximity. The message should reflect the actual content (e.g., "No eateries found nearby" or indicate a location/data loading issue).

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

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/screens/NearestScreen.kt`
around lines 88 - 89, The empty-state message in NearestScreen is incorrect;
locate the Text composable inside the NearestScreen composable (the Text whose
text is "You currently have no favorite eateries!") and update its string to a
context-appropriate message such as "No eateries found nearby" (or a localized
string resource like R.string.no_eateries_nearby), ensuring the displayed
message matches the NearestScreen's purpose and data-loading/location states.
app/src/main/java/com/cornellappdev/android/eatery/ui/screens/SearchScreen.kt (1)

237-239: ⚠️ Potential issue | 🟡 Minor

Potential NPE with force-unwrapped eatery.id.

Using eatery.id!! as a key in the favorites LazyRow can crash if an eatery has a null ID.

🛡️ Suggested safer key
 items(items = favorites, key = { eatery ->
-    eatery.id!!
+    eatery.id ?: eatery.hashCode()
 }) { eatery ->
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/screens/SearchScreen.kt`
around lines 237 - 239, The key selector in the favorites LazyRow uses a
force-unwrapped ID (items(items = favorites, key = { eatery -> eatery.id!! }))
which can NPE; change the key to a null-safe value such as using eatery.id ?:
fallback (e.g., eatery.name, eatery.slug, or eatery.hashCode()) so the key
generation never uses !!; update the items(...) key lambda to return that safe
fallback to avoid crashes while keeping key uniqueness.
app/src/main/java/com/cornellappdev/android/eatery/ui/screens/FavoritesScreen.kt (1)

203-207: ⚠️ Potential issue | 🟡 Minor

Potential NPE with force-unwrapped eatery.id.

Using eatery.id!! as a key assumes the ID is always non-null. If an eatery with a null ID enters this list, the app will crash. Consider using a safe fallback or filtering out eateries with null IDs upstream.

🛡️ Suggested safer key
 items(
     items = favoriteEateries,
     key = { eatery ->
-        eatery.id!!
+        eatery.id ?: eatery.hashCode()
     }) { eatery ->
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/screens/FavoritesScreen.kt`
around lines 203 - 207, The use of a force-unwrapped key eatery.id!! inside the
composable items call (items(items = favoriteEateries, key = { eatery ->
eatery.id!! })) can crash if an Eatery has a null id; update the items key to
use a safe fallback (e.g., eatery.id ?: eatery.hashCode().toString() or another
stable unique fallback) or filter out null-id entries from favoriteEateries
before rendering so the items key is never null; modify the items(...)
invocation and/or the producer of favoriteEateries accordingly (refer to
favoriteEateries and the items { eatery -> ... } key lambda).
🧹 Nitpick comments (2)
app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/SearchViewModel.kt (2)

132-132: Verify ordering of reversed().take(10).distinct().

The current order applies distinct() after take(10), which means if there are duplicates within the first 10 reversed items, you'll get fewer than 10 results. If the intent is to show up to 10 unique recent searches (most recent first), consider:

recents.reversed().distinct().take(10)

This ensures deduplication happens before limiting, giving you the 10 most recent unique searches.

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

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/SearchViewModel.kt`
at line 132, The assignment to recentSearches in SearchViewModel uses
recents.reversed().take(10).distinct(), which can yield fewer than 10 entries
when duplicates appear in the first 10; change the call order to perform
deduplication before limiting by using recents.reversed().distinct().take(10) so
you get up to 10 most-recent unique searches (update the expression that sets
recentSearches in SearchViewModel accordingly).

42-42: Consider thread-safety and memory management for eateryFlowCache.

The mutableMapOf cache stores StateFlow instances indefinitely. Two potential concerns:

  1. Memory: The cache grows without bounds as users browse eateries. Consider using a size-limited cache or weak references.
  2. Thread-safety: While ViewModel operations typically run on Main, getOrPut is not atomic. If observeEatery is called concurrently with the same ID, duplicate flows could be created.
♻️ Thread-safe alternative using ConcurrentHashMap
+import java.util.concurrent.ConcurrentHashMap

-    private val eateryFlowCache = mutableMapOf<Int, StateFlow<EateryApiResponse<Eatery>>>()
+    private val eateryFlowCache = ConcurrentHashMap<Int, StateFlow<EateryApiResponse<Eatery>>>()

Also applies to: 206-213

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

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/SearchViewModel.kt`
at line 42, eateryFlowCache is an unbounded, non-thread-safe mutableMap that can
leak memory and create duplicate StateFlows when observeEatery (and related
logic around lines 206-213) is called concurrently; replace the map with a
thread-safe bounded cache (e.g., a ConcurrentHashMap or an LRU size-limited
cache holding StateFlow<EateryApiResponse<Eatery>> or WeakReferences to those
flows) and atomically insert-or-retrieve entries (or synchronize access) so
getOrPut-style creation of flows is safe under concurrency and the cache cannot
grow unbounded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt`:
- Around line 125-127: onLoginPressed currently only flips _isLoginLoadingFlow
to true but leaves any previous NetworkUiError live; modify onLoginPressed to
also clear the stored network error (e.g., set _networkUiErrorFlow /
_networkUiError.value to null or call the ViewModel's error-clear helper) so a
new login attempt doesn't show stale errors while loading. Target the
onLoginPressed method and the backing error flow/variable (e.g.,
_networkUiErrorFlow or NetworkUiError) and ensure the error is cleared before
setting _isLoginLoadingFlow to true.
- Around line 43-83: The UI shows the login screen whenever loadedUser == null,
which flashes during async session restore; add a session-restoring flag and use
it in the uiState combine: introduce a MutableStateFlow<Boolean> (e.g.
_isRestoringSession = MutableStateFlow(true)), include it in the combine
alongside userRepository.loadedUser and compute isLoginState as
(!isRestoringSession && loadedUser == null), then set _isRestoringSession.value
= false after init's restore flow finishes (e.g. after getFinancials() returns
or when getAccountRepository.isLoggedIn() check completes) so the UI only shows
login once restore is done; update references in uiState construction (symbols:
_isRestoringSession, uiState, userRepository.loadedUser, getFinancials(), init).

---

Outside diff comments:
In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/screens/FavoritesScreen.kt`:
- Around line 203-207: The use of a force-unwrapped key eatery.id!! inside the
composable items call (items(items = favoriteEateries, key = { eatery ->
eatery.id!! })) can crash if an Eatery has a null id; update the items key to
use a safe fallback (e.g., eatery.id ?: eatery.hashCode().toString() or another
stable unique fallback) or filter out null-id entries from favoriteEateries
before rendering so the items key is never null; modify the items(...)
invocation and/or the producer of favoriteEateries accordingly (refer to
favoriteEateries and the items { eatery -> ... } key lambda).

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/screens/NearestScreen.kt`:
- Around line 104-108: The key provider for the LazyColumn items uses a
force-unwrapped ID (items(... key = { eatery -> eatery.id!! })) which can throw
an NPE; update the key logic in NearestScreen to use a null-safe fallback (e.g.,
key = { eatery -> eatery.id ?: eatery.hashCode() } or combine a stable field
like name/index) so it never force-unwraps, mirroring the safer approach used in
FavoritesScreen.
- Around line 88-89: The empty-state message in NearestScreen is incorrect;
locate the Text composable inside the NearestScreen composable (the Text whose
text is "You currently have no favorite eateries!") and update its string to a
context-appropriate message such as "No eateries found nearby" (or a localized
string resource like R.string.no_eateries_nearby), ensuring the displayed
message matches the NearestScreen's purpose and data-loading/location states.

In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/screens/SearchScreen.kt`:
- Around line 237-239: The key selector in the favorites LazyRow uses a
force-unwrapped ID (items(items = favorites, key = { eatery -> eatery.id!! }))
which can NPE; change the key to a null-safe value such as using eatery.id ?:
fallback (e.g., eatery.name, eatery.slug, or eatery.hashCode()) so the key
generation never uses !!; update the items(...) key lambda to return that safe
fallback to avoid crashes while keeping key uniqueness.

---

Nitpick comments:
In
`@app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/SearchViewModel.kt`:
- Line 132: The assignment to recentSearches in SearchViewModel uses
recents.reversed().take(10).distinct(), which can yield fewer than 10 entries
when duplicates appear in the first 10; change the call order to perform
deduplication before limiting by using recents.reversed().distinct().take(10) so
you get up to 10 most-recent unique searches (update the expression that sets
recentSearches in SearchViewModel accordingly).
- Line 42: eateryFlowCache is an unbounded, non-thread-safe mutableMap that can
leak memory and create duplicate StateFlows when observeEatery (and related
logic around lines 206-213) is called concurrently; replace the map with a
thread-safe bounded cache (e.g., a ConcurrentHashMap or an LRU size-limited
cache holding StateFlow<EateryApiResponse<Eatery>> or WeakReferences to those
flows) and atomically insert-or-retrieve entries (or synchronize access) so
getOrPut-style creation of flows is safe under concurrency and the cache cannot
grow unbounded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6948e272-6db4-4826-83d7-620c40148e48

📥 Commits

Reviewing files that changed from the base of the PR and between c5b2382 and 49088b0.

📒 Files selected for processing (10)
  • app/src/main/java/com/cornellappdev/android/eatery/ui/screens/FavoritesScreen.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/screens/HomeScreen.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/screens/NearestScreen.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/screens/ProfileScreen.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/screens/SearchScreen.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/FavoritesViewModel.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/HomeViewModel.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/LoginViewModel.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/NearestViewModel.kt
  • app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/SearchViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/cornellappdev/android/eatery/ui/screens/ProfileScreen.kt

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.

2 participants