Skip to content

feat: Add data migration and robust system sync#136

Merged
rainxchzed merged 2 commits intomainfrom
updated-refresh-logic
Jan 4, 2026
Merged

feat: Add data migration and robust system sync#136
rainxchzed merged 2 commits intomainfrom
updated-refresh-logic

Conversation

@rainxchzed
Copy link
Owner

@rainxchzed rainxchzed commented Jan 4, 2026

This introduces a robust data synchronization and migration mechanism at app startup and before checking for updates.

Key changes:

  • System Package Sync: Before loading apps, it now cross-references the database with the system's installed packages. Apps that are no longer installed on the system are removed from the database.
  • Data Migration: For existing tracked apps missing versionName and versionCode, this change populates them using data from the package manager on Android or falls back to using the release tag on other platforms.
  • The checkAllForUpdates function now leverages the more efficient installedAppsRepository.checkAllForUpdates() after performing the sync and migration, instead of iterating and checking each app individually within the ViewModel.

Summary by CodeRabbit

  • Bug Fixes

    • Improved app data synchronization to remove entries for uninstalled packages and avoid stale DB state.
    • Enhanced version info migration so apps missing version details are populated reliably.
  • Refactor

    • Consolidated and simplified update-check flow for more efficient, centralized update checks.
  • Other

    • Migration now respects platform type to choose appropriate version-source during synchronization.

✏️ Tip: You can customize this high-level summary in your review settings.

This introduces a robust data synchronization and migration mechanism at app startup and before checking for updates.

Key changes:
- **System Package Sync:** Before loading apps, it now cross-references the database with the system's installed packages. Apps that are no longer installed on the system are removed from the database.
- **Data Migration:** For existing tracked apps missing `versionName` and `versionCode`, this change populates them using data from the package manager on Android or falls back to using the release tag on other platforms.
- The `checkAllForUpdates` function now leverages the more efficient `installedAppsRepository.checkAllForUpdates()` after performing the sync and migration, instead of iterating and checking each app individually within the ViewModel.
@rainxchzed rainxchzed changed the title feat(apps): Add data migration and robust system sync feat: Add data migration and robust system sync Jan 4, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 4, 2026

Walkthrough

Adds system-installed-package synchronization and migration to AppsViewModel and DetailsViewModel: on load and before update checks the view models compare system packages with DB entries, remove stale records, and populate missing version fields (Android vs desktop). AppsViewModel constructor gained a new platform: Platform dependency.

Changes

Cohort / File(s) Summary
Apps ViewModel
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt
New syncSystemExistenceAndMigrate() suspend function that reads system-installed packages, compares with DB entries, deletes stale DB records, and migrates missing installedVersionName/installedVersionCode (Android-specific fallback to tag/version otherwise). checkAllForUpdates() replaced to call the new sync then repository checkAllForUpdates(). Constructor now requires platform: Platform. Added coroutine/flow imports (Dispatchers, withContext, first).
Details ViewModel
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt
Added syncSystemExistenceAndMigrate() and invoke it in loadInitial and before update checks. Simplified installed-app DB validation path (clear pending when app exists, otherwise retain DB data). Adjusted error message text and added coroutine/flow imports.
DI / Wiring
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt
VM provider in appsModule updated to pass platform = get() into AppsViewModel constructor.
Attention Areas
composeApp/.../feature/.../presentation/*.kt
Review platform-specific migration logic (Android vs desktop), concurrency context uses (IO withContext(Dispatchers.IO)), and DB delete/migrate semantics to ensure no data loss. Confirm DI change is wired in all module instantiations.

Sequence Diagram

sequenceDiagram
    participant VM as ViewModel
    participant Sys as SystemPackageManager
    participant DB as InstalledAppsRepository / Database
    participant Repo as AppsRepository

    VM->>VM: loadApps() / loadInitial()
    VM->>Sys: readInstalledPackages() (IO)
    Sys-->>VM: installed package list
    VM->>DB: queryInstalledApps()
    DB-->>VM: DB app records
    alt DB entries for packages not installed
        VM->>DB: deleteStaleInstalledApps()
        DB-->>VM: deletion confirmation
    end
    alt Missing version fields on DB entry
        VM->>Sys: fetchVersionFromSystem() (Android) / deriveFromTag()
        Sys-->>VM: version info
        VM->>DB: updateInstalledAppVersion()
        DB-->>VM: update confirmation
    end
    VM->>Repo: checkAllForUpdates()
    Repo-->>VM: update results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 The rabbit hops through code and crates,
Syncing packages, closing stale gates.
Versions migrate, records renew,
Platform-aware, the checks run true.
A tiny hop — a big update!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 accurately summarizes the main changes: adding data migration and system synchronization logic across the ViewModels.
✨ Finishing touches
  • 📝 Generate docstrings

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

🧹 Nitpick comments (1)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt (1)

136-153: Duplicated fallback logic between Android null-case and desktop.

Lines 137-143 (Android when systemInfo == null) and lines 146-151 (desktop) have identical logic. Consider extracting to reduce duplication.

🔎 Proposed refactor
-                    } else if (app.installedVersionName == null) {
-                        if (platform.type == PlatformType.ANDROID) {
-                            val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName)
-                            if (systemInfo != null) {
-                                installedAppsRepository.updateApp(app.copy(
-                                    installedVersionName = systemInfo.versionName,
-                                    installedVersionCode = systemInfo.versionCode,
-                                    latestVersionName = systemInfo.versionName,
-                                    latestVersionCode = systemInfo.versionCode
-                                ))
-                                Logger.d { "Migrated ${app.packageName}: set versionName/code from system" }
-                            } else {
-                                installedAppsRepository.updateApp(app.copy(
-                                    installedVersionName = app.installedVersion,
-                                    installedVersionCode = 0L,
-                                    latestVersionName = app.installedVersion,
-                                    latestVersionCode = 0L
-                                ))
-                                Logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" }
-                            }
-                        } else {
-                            installedAppsRepository.updateApp(app.copy(
-                                installedVersionName = app.installedVersion,
-                                installedVersionCode = 0L,
-                                latestVersionName = app.installedVersion,
-                                latestVersionCode = 0L
-                            ))
-                            Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag as versionName" }
-                        }
-                    }
+                    } else if (app.installedVersionName == null) {
+                        val systemInfo = if (platform.type == PlatformType.ANDROID) {
+                            packageMonitor.getInstalledPackageInfo(app.packageName)
+                        } else null
+
+                        if (systemInfo != null) {
+                            installedAppsRepository.updateApp(app.copy(
+                                installedVersionName = systemInfo.versionName,
+                                installedVersionCode = systemInfo.versionCode,
+                                latestVersionName = systemInfo.versionName,
+                                latestVersionCode = systemInfo.versionCode
+                            ))
+                            Logger.d { "Migrated ${app.packageName}: set versionName/code from system" }
+                        } else {
+                            installedAppsRepository.updateApp(app.copy(
+                                installedVersionName = app.installedVersion,
+                                installedVersionCode = 0L,
+                                latestVersionName = app.installedVersion,
+                                latestVersionCode = 0L
+                            ))
+                            val platformLabel = if (platform.type == PlatformType.ANDROID) "" else " (desktop)"
+                            Logger.d { "Migrated ${app.packageName}$platformLabel: fallback to tag as versionName" }
+                        }
+                    }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f8f096 and dd7f6ce.

📒 Files selected for processing (1)
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt
🔇 Additional comments (2)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt (2)

49-51: LGTM!

The new platform dependency is properly added and follows the existing pattern. This enables platform-specific migration logic in the sync function.


78-80: LGTM!

Calling the sync/migration before collecting apps ensures the database is consistent with system state before UI population. The error handling in syncSystemExistenceAndMigrate() ensures failures don't block app loading.

Comment on lines +115 to +162
private suspend fun syncSystemExistenceAndMigrate() {
withContext(Dispatchers.IO) {
try {
val installedPackageNames = packageMonitor.getAllInstalledPackageNames()
val appsInDb = installedAppsRepository.getAllInstalledApps().first()

appsInDb.forEach { app ->
if (!installedPackageNames.contains(app.packageName)) {
Logger.d { "App ${app.packageName} no longer installed (not in system packages), removing from DB" }
installedAppsRepository.deleteInstalledApp(app.packageName)
} else if (app.installedVersionName == null) {
if (platform.type == PlatformType.ANDROID) {
val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName)
if (systemInfo != null) {
installedAppsRepository.updateApp(app.copy(
installedVersionName = systemInfo.versionName,
installedVersionCode = systemInfo.versionCode,
latestVersionName = systemInfo.versionName,
latestVersionCode = systemInfo.versionCode
))
Logger.d { "Migrated ${app.packageName}: set versionName/code from system" }
} else {
installedAppsRepository.updateApp(app.copy(
installedVersionName = app.installedVersion,
installedVersionCode = 0L,
latestVersionName = app.installedVersion,
latestVersionCode = 0L
))
Logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" }
}
} else {
installedAppsRepository.updateApp(app.copy(
installedVersionName = app.installedVersion,
installedVersionCode = 0L,
latestVersionName = app.installedVersion,
latestVersionCode = 0L
))
Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag as versionName" }
}
}
}

Logger.d { "Robust system existence sync and data migration completed" }
} catch (e: Exception) {
Logger.e { "Failed to sync existence or migrate data: ${e.message}" }
}
}
}
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

Consider re-throwing CancellationException to respect coroutine cancellation.

The catch block on line 158 catches all exceptions including CancellationException. This can cause the coroutine to appear successful when it was actually cancelled, potentially leading to inconsistent state during ViewModel cleanup.

🔎 Proposed fix
             Logger.d { "Robust system existence sync and data migration completed" }
         } catch (e: Exception) {
+            if (e is CancellationException) throw e
             Logger.e { "Failed to sync existence or migrate data: ${e.message}" }
         }
📝 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
private suspend fun syncSystemExistenceAndMigrate() {
withContext(Dispatchers.IO) {
try {
val installedPackageNames = packageMonitor.getAllInstalledPackageNames()
val appsInDb = installedAppsRepository.getAllInstalledApps().first()
appsInDb.forEach { app ->
if (!installedPackageNames.contains(app.packageName)) {
Logger.d { "App ${app.packageName} no longer installed (not in system packages), removing from DB" }
installedAppsRepository.deleteInstalledApp(app.packageName)
} else if (app.installedVersionName == null) {
if (platform.type == PlatformType.ANDROID) {
val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName)
if (systemInfo != null) {
installedAppsRepository.updateApp(app.copy(
installedVersionName = systemInfo.versionName,
installedVersionCode = systemInfo.versionCode,
latestVersionName = systemInfo.versionName,
latestVersionCode = systemInfo.versionCode
))
Logger.d { "Migrated ${app.packageName}: set versionName/code from system" }
} else {
installedAppsRepository.updateApp(app.copy(
installedVersionName = app.installedVersion,
installedVersionCode = 0L,
latestVersionName = app.installedVersion,
latestVersionCode = 0L
))
Logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" }
}
} else {
installedAppsRepository.updateApp(app.copy(
installedVersionName = app.installedVersion,
installedVersionCode = 0L,
latestVersionName = app.installedVersion,
latestVersionCode = 0L
))
Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag as versionName" }
}
}
}
Logger.d { "Robust system existence sync and data migration completed" }
} catch (e: Exception) {
Logger.e { "Failed to sync existence or migrate data: ${e.message}" }
}
}
}
private suspend fun syncSystemExistenceAndMigrate() {
withContext(Dispatchers.IO) {
try {
val installedPackageNames = packageMonitor.getAllInstalledPackageNames()
val appsInDb = installedAppsRepository.getAllInstalledApps().first()
appsInDb.forEach { app ->
if (!installedPackageNames.contains(app.packageName)) {
Logger.d { "App ${app.packageName} no longer installed (not in system packages), removing from DB" }
installedAppsRepository.deleteInstalledApp(app.packageName)
} else if (app.installedVersionName == null) {
if (platform.type == PlatformType.ANDROID) {
val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName)
if (systemInfo != null) {
installedAppsRepository.updateApp(app.copy(
installedVersionName = systemInfo.versionName,
installedVersionCode = systemInfo.versionCode,
latestVersionName = systemInfo.versionName,
latestVersionCode = systemInfo.versionCode
))
Logger.d { "Migrated ${app.packageName}: set versionName/code from system" }
} else {
installedAppsRepository.updateApp(app.copy(
installedVersionName = app.installedVersion,
installedVersionCode = 0L,
latestVersionName = app.installedVersion,
latestVersionCode = 0L
))
Logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" }
}
} else {
installedAppsRepository.updateApp(app.copy(
installedVersionName = app.installedVersion,
installedVersionCode = 0L,
latestVersionName = app.installedVersion,
latestVersionCode = 0L
))
Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag as versionName" }
}
}
}
Logger.d { "Robust system existence sync and data migration completed" }
} catch (e: Exception) {
if (e is CancellationException) throw e
Logger.e { "Failed to sync existence or migrate data: ${e.message}" }
}
}
}
🤖 Prompt for AI Agents
In
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt
around lines 115 to 162, the catch block currently swallows all Exceptions
including CancellationException; update the handler so that if the caught
exception is a CancellationException it is re-thrown (or not caught) to allow
coroutine cancellation to propagate, otherwise handle/log the error as before.
Ensure you import or reference kotlinx.coroutines.CancellationException if
needed and rethrow immediately when detected before logging or swallowing other
exceptions.

Comment on lines +164 to +174
private fun checkAllForUpdates() {
viewModelScope.launch {
try {
syncSystemExistenceAndMigrate()

installedAppsRepository.checkAllForUpdates()
} catch (e: Exception) {
Logger.e { "Check all for updates failed: ${e.message}" }
}
}
}
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

Same CancellationException handling issue.

Similar to syncSystemExistenceAndMigrate(), this catch block swallows cancellation. For consistency and proper coroutine behavior, re-throw CancellationException.

🔎 Proposed fix
         viewModelScope.launch {
             try {
                 syncSystemExistenceAndMigrate()

                 installedAppsRepository.checkAllForUpdates()
             } catch (e: Exception) {
+                if (e is CancellationException) throw e
                 Logger.e { "Check all for updates failed: ${e.message}" }
             }
         }
📝 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
private fun checkAllForUpdates() {
viewModelScope.launch {
try {
syncSystemExistenceAndMigrate()
installedAppsRepository.checkAllForUpdates()
} catch (e: Exception) {
Logger.e { "Check all for updates failed: ${e.message}" }
}
}
}
private fun checkAllForUpdates() {
viewModelScope.launch {
try {
syncSystemExistenceAndMigrate()
installedAppsRepository.checkAllForUpdates()
} catch (e: Exception) {
if (e is CancellationException) throw e
Logger.e { "Check all for updates failed: ${e.message}" }
}
}
}
🤖 Prompt for AI Agents
In
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt
around lines 164 to 174, the catch block for Exception swallows
CancellationException; change the catch to re-throw CancellationException and
only log other exceptions — i.e., inside the catch check if the caught exception
is a CancellationException and throw it, otherwise log the error message as
before.

This commit introduces a new `syncSystemExistenceAndMigrate` function to centralize application state management. This function is now called at the beginning of the `loadData` and `CheckForUpdates` actions to ensure the database accurately reflects the system's installed packages.

Key changes:
-   **Removed Complex Sync Logic**: Deleted a large, complex block of code responsible for syncing application state from within the `loadData` flow.
-   **Centralized Sync Function**: Created `syncSystemExistenceAndMigrate()` to handle:
    -   Removing apps from the database that are no longer installed on the system.
    -   Migrating legacy database entries that lack version name/code by populating them from system package info.
-   **Simplified App Loading**: The process for loading an installed app's state is now simplified, relying on the newly synchronized database.
-   **Dependency Injection**: Added the `Platform` dependency to the `DetailsViewModel` to support platform-specific migration logic.
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

🧹 Nitpick comments (4)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (4)

84-85: Consider propagating sync failures for error recovery.

The sync function catches exceptions internally but doesn't propagate them. If the sync fails, loadInitial continues without knowledge of the failure, potentially leading to inconsistent state assumptions. Consider returning a success/failure indicator or propagating exceptions to allow the caller to decide whether to proceed or show an error to the user.


135-159: Redundant system check after synchronization.

Lines 143-149 check if the package is installed on the system, but syncSystemExistenceAndMigrate() has already removed apps not present in system packages (line 227-229). This additional packageMonitor.isPackageInstalled() check appears redundant and adds unnecessary I/O overhead.

If the pending status clearing is the only goal here, consider whether the sync function should handle that as well, or simply trust that if a DB entry exists after sync, the package is installed.


264-266: Propagate errors to enable caller recovery.

The catch block logs the error but doesn't propagate it or return a failure indicator. Callers (like loadInitial and CheckForUpdates) have no way to know if the sync failed, which may lead them to operate on stale or inconsistent data.

Consider either:

  • Re-throwing the exception after logging
  • Returning a Result<Unit> to indicate success/failure
  • Emitting an error event to the UI

363-381: Consider caching or rate-limiting sync operations.

Calling syncSystemExistenceAndMigrate() on every "Check for Updates" action performs a full scan of all tracked apps and system packages, even though the user is only checking one app. If there are many tracked apps, this could introduce noticeable latency.

Consider:

  • Rate-limiting sync (e.g., skip if run within last 5 minutes)
  • Only syncing on app startup and periodic background refresh
  • Caching system package list with TTL

The current approach is safe and correct, but may feel slow to users with many tracked apps.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd7f6ce and 6668a49.

📒 Files selected for processing (2)
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt
🧰 Additional context used
🧬 Code graph analysis (1)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (1)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt (1)
  • syncSystemExistenceAndMigrate (115-162)
🔇 Additional comments (2)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt (1)

250-251: LGTM! Dependency wiring follows established patterns.

The addition of platform = get() to the AppsViewModel instantiation is consistent with how other ViewModels in the file receive the platform dependency (MainViewModel, HomeViewModel, DetailsViewModel). The trailing comma on line 250 is good Kotlin practice.

AppsViewModel constructor correctly includes the platform: Platform parameter at line 50 and actively uses it in the migration logic.

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (1)

157-157: Improved error message clarity.

The error message now correctly reflects the operation being performed ("load" rather than "check"), which improves debugging clarity.

Comment on lines +220 to +268
private suspend fun syncSystemExistenceAndMigrate() {
withContext(Dispatchers.IO) {
try {
val installedPackageNames = packageMonitor.getAllInstalledPackageNames()
val appsInDb = installedAppsRepository.getAllInstalledApps().first()

appsInDb.forEach { app ->
if (!installedPackageNames.contains(app.packageName)) {
Logger.d { "App ${app.packageName} no longer installed, removing from DB" }
installedAppsRepository.deleteInstalledApp(app.packageName)
} else if (app.installedVersionName == null) {
// Migration logic for apps missing version data
if (platform.type == PlatformType.ANDROID) {
val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName)
if (systemInfo != null) {
installedAppsRepository.updateApp(app.copy(
installedVersionName = systemInfo.versionName,
installedVersionCode = systemInfo.versionCode,
latestVersionName = systemInfo.versionName,
latestVersionCode = systemInfo.versionCode
))
Logger.d { "Migrated ${app.packageName}: set versionName/code from system" }
} else {
installedAppsRepository.updateApp(app.copy(
installedVersionName = app.installedVersion,
installedVersionCode = 0L,
latestVersionName = app.installedVersion,
latestVersionCode = 0L
))
Logger.d { "Migrated ${app.packageName}: fallback to tag" }
}
} else {
installedAppsRepository.updateApp(app.copy(
installedVersionName = app.installedVersion,
installedVersionCode = 0L,
latestVersionName = app.installedVersion,
latestVersionCode = 0L
))
Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag" }
}
}
}

Logger.d { "System existence sync and data migration completed" }
} catch (e: Exception) {
Logger.e { "Failed to sync existence or migrate data: ${e.message}" }
}
}
}
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

Extract duplicated function to shared utility.

This function is identical to syncSystemExistenceAndMigrate() in AppsViewModel.kt (lines 114-161). Duplicating 48 lines of complex synchronization logic violates DRY principles and creates a maintenance burden—any bug fix or improvement must be applied in two places.

📝 Suggested approach

Extract this function to a shared use case or repository helper class that both ViewModels can depend on. For example:

// In a new file: SyncInstalledAppsUseCase.kt
class SyncInstalledAppsUseCase(
    private val packageMonitor: PackageMonitor,
    private val installedAppsRepository: InstalledAppsRepository,
    private val platform: Platform
) {
    suspend operator fun invoke() {
        // ... existing implementation
    }
}

Then inject this use case into both ViewModels and call it instead of maintaining duplicate implementations.

Based on learnings, code duplication across ViewModels should be extracted to shared components.

Comment on lines +226 to +261
appsInDb.forEach { app ->
if (!installedPackageNames.contains(app.packageName)) {
Logger.d { "App ${app.packageName} no longer installed, removing from DB" }
installedAppsRepository.deleteInstalledApp(app.packageName)
} else if (app.installedVersionName == null) {
// Migration logic for apps missing version data
if (platform.type == PlatformType.ANDROID) {
val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName)
if (systemInfo != null) {
installedAppsRepository.updateApp(app.copy(
installedVersionName = systemInfo.versionName,
installedVersionCode = systemInfo.versionCode,
latestVersionName = systemInfo.versionName,
latestVersionCode = systemInfo.versionCode
))
Logger.d { "Migrated ${app.packageName}: set versionName/code from system" }
} else {
installedAppsRepository.updateApp(app.copy(
installedVersionName = app.installedVersion,
installedVersionCode = 0L,
latestVersionName = app.installedVersion,
latestVersionCode = 0L
))
Logger.d { "Migrated ${app.packageName}: fallback to tag" }
}
} else {
installedAppsRepository.updateApp(app.copy(
installedVersionName = app.installedVersion,
installedVersionCode = 0L,
latestVersionName = app.installedVersion,
latestVersionCode = 0L
))
Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag" }
}
}
}
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 transaction boundaries to ensure database consistency.

The function performs multiple database operations (delete, update) in a loop without transaction boundaries. If the process fails midway (e.g., exception on iteration 5 of 10), the database will be in an inconsistent state—some apps removed or migrated, others not.

Consider wrapping the entire forEach loop in a database transaction or handling partial failures gracefully.

💡 Example transaction approach
// Pseudocode - adjust based on your database layer's transaction API
installedAppsRepository.runInTransaction {
    appsInDb.forEach { app ->
        if (!installedPackageNames.contains(app.packageName)) {
            installedAppsRepository.deleteInstalledApp(app.packageName)
        } else if (app.installedVersionName == null) {
            // ... migration logic
        }
    }
}

@rainxchzed rainxchzed merged commit 1d556ab into main Jan 4, 2026
2 checks passed
@rainxchzed rainxchzed deleted the updated-refresh-logic branch January 4, 2026 09:47
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