From 42c60b374ec322c1905ed934589f1aaf618f77b6 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 3 Dec 2025 11:51:35 -0300 Subject: [PATCH] Try to fix molecule issues We were running the presenting on the IoDispatcher and this seemed to have caused random crashes. --- .../org/fdroid/ui/apps/MyAppsViewModel.kt | 20 +++-- .../fdroid/ui/details/AppDetailsViewModel.kt | 36 ++++---- .../org/fdroid/ui/details/DetailsPresenter.kt | 89 ++++++++++++------- .../fdroid/ui/discover/DiscoverViewModel.kt | 23 ++--- .../org/fdroid/ui/lists/AppListViewModel.kt | 27 +++--- .../ui/repositories/RepositoriesViewModel.kt | 18 ++-- .../details/RepoDetailsViewModel.kt | 18 ++-- 7 files changed, 135 insertions(+), 96 deletions(-) diff --git a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt index a9e8c32ec..33f231828 100644 --- a/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt @@ -71,15 +71,17 @@ class MyAppsViewModel @Inject constructor( private val searchQuery = savedStateHandle.getMutableStateFlow("query", "") private val sortOrder = savedStateHandle.getMutableStateFlow("sort", AppListSortOrder.NAME) - val myAppsModel: StateFlow = moleculeScope.launchMolecule(mode = ContextClock) { - MyAppsPresenter( - appUpdatesFlow = updates, - appInstallStatesFlow = appInstallManager.appInstallStates, - appsWithIssuesFlow = updatesManager.appsWithIssues, - installedAppsFlow = installedAppItems, - searchQueryFlow = searchQuery, - sortOrderFlow = sortOrder, - ) + val myAppsModel: StateFlow by lazy(LazyThreadSafetyMode.NONE) { + moleculeScope.launchMolecule(mode = ContextClock) { + MyAppsPresenter( + appUpdatesFlow = updates, + appInstallStatesFlow = appInstallManager.appInstallStates, + appsWithIssuesFlow = updatesManager.appsWithIssues, + installedAppsFlow = installedAppItems, + searchQueryFlow = searchQuery, + sortOrderFlow = sortOrder, + ) + } } fun updateAll() { diff --git a/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt index 079a38c1b..2cf59e585 100644 --- a/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt @@ -10,7 +10,8 @@ import androidx.annotation.UiThread import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.application import androidx.lifecycle.viewModelScope -import app.cash.molecule.RecompositionMode.Immediate +import app.cash.molecule.AndroidUiDispatcher +import app.cash.molecule.RecompositionMode.ContextClock import app.cash.molecule.launchMolecule import coil3.SingletonImageLoader import dagger.hilt.android.lifecycle.HiltViewModel @@ -54,22 +55,25 @@ class AppDetailsViewModel @Inject constructor( private val log = KotlinLogging.logger { } private val packageInfoFlow = MutableStateFlow(null) private val currentRepoIdFlow = MutableStateFlow(null) + private val moleculeScope = + CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) - val appDetails: StateFlow = viewModelScope.launchMolecule( - context = scope.coroutineContext, mode = Immediate, - ) { - DetailsPresenter( - db = db, - repoManager = repoManager, - repoPreLoader = repoPreLoader, - updateChecker = updateChecker, - settingsManager = settingsManager, - appInstallManager = appInstallManager, - viewModel = this, - packageInfoFlow = packageInfoFlow, - currentRepoIdFlow = currentRepoIdFlow, - appsWithIssuesFlow = updatesManager.appsWithIssues, - ) + val appDetails: StateFlow by lazy(LazyThreadSafetyMode.NONE) { + moleculeScope.launchMolecule(mode = ContextClock) { + DetailsPresenter( + db = db, + scope = scope, + repoManager = repoManager, + repoPreLoader = repoPreLoader, + updateChecker = updateChecker, + settingsManager = settingsManager, + appInstallManager = appInstallManager, + viewModel = this, + packageInfoFlow = packageInfoFlow, + currentRepoIdFlow = currentRepoIdFlow, + appsWithIssuesFlow = updatesManager.appsWithIssues, + ) + } } fun setAppDetails(packageName: String) { diff --git a/next/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/next/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index 95341045d..6168c730a 100644 --- a/next/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/next/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -5,13 +5,19 @@ import android.net.Uri import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.core.content.pm.PackageInfoCompat.getLongVersionCode import androidx.core.net.toUri import androidx.core.os.LocaleListCompat import androidx.lifecycle.asFlow +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.withContext import org.fdroid.UpdateChecker +import org.fdroid.database.App +import org.fdroid.database.AppPrefs +import org.fdroid.database.AppVersion import org.fdroid.database.FDroidDatabase import org.fdroid.database.Repository import org.fdroid.index.RepoManager @@ -30,6 +36,7 @@ private const val TAG = "DetailsPresenter" @Composable fun DetailsPresenter( db: FDroidDatabase, + scope: CoroutineScope, repoManager: RepoManager, repoPreLoader: RepoPreLoader, updateChecker: UpdateChecker, @@ -45,41 +52,56 @@ fun DetailsPresenter( val packageInfo = packagePair.packageInfo val currentRepoId = currentRepoIdFlow.collectAsState().value val appsWithIssues = appsWithIssuesFlow.collectAsState().value - val app = if (currentRepoId == null) { - val flow = remember { - db.getAppDao().getApp(packageName).asFlow() + val appDao = db.getAppDao() + val app = produceState(null, currentRepoId) { + withContext(scope.coroutineContext) { + if (currentRepoId == null) { + val flow = appDao.getApp(packageName).asFlow() + flow.collect { value = it } + } else { + value = appDao.getApp(currentRepoId, packageName) + } } - flow.collectAsState(null).value - } else { - db.getAppDao().getApp(currentRepoId, packageName) - } ?: return null - val repo = repoManager.getRepository(app.repoId) ?: return null - val repositories = remember(packageName) { - val repos = db.getAppDao().getRepositoryIdsForApp(packageName).mapNotNull { repoId -> - repoManager.getRepository(repoId) + }.value ?: return null + val repo = produceState(null) { + withContext(scope.coroutineContext) { + value = repoManager.getRepository(app.repoId) } - // show repo chooser only if - // * app is in more than one repo, or - // * app is from a non-default repo - if (repos.size > 1) repos - else if (repo.address in repoPreLoader.defaultRepoAddresses) emptyList() - else repos - } + }.value ?: return null + val repositories = produceState(emptyList(), packageName) { + withContext(scope.coroutineContext) { + val repos = appDao.getRepositoryIdsForApp(packageName).mapNotNull { repoId -> + repoManager.getRepository(repoId) + } + // show repo chooser only if + // * app is in more than one repo, or + // * app is from a non-default repo + value = if (repos.size > 1) repos + else if (repo.address in repoPreLoader.defaultRepoAddresses) emptyList() + else repos + } + }.value val installState = appInstallManager.getAppFlow(packageName).collectAsState(InstallState.Unknown).value - val versionsFlow = remember(currentRepoId) { - if (currentRepoId == null) { - db.getVersionDao().getAppVersions(app.repoId, packageName).asFlow() - } else { - db.getVersionDao().getAppVersions(currentRepoId, packageName).asFlow() + val versions = produceState?>(null, currentRepoId) { + withContext(scope.coroutineContext) { + if (currentRepoId == null) { + db.getVersionDao().getAppVersions(app.repoId, packageName).asFlow().collect { + value = it + } + } else { + db.getVersionDao().getAppVersions(currentRepoId, packageName).asFlow().collect { + value = it + } + } } - } - val versions = versionsFlow.collectAsState(null).value - val appPrefsFlow = remember(packageName) { - db.getAppPrefsDao().getAppPrefs(packageName).asFlow() - } - val appPrefs = appPrefsFlow.collectAsState(null).value + }.value + val appPrefs = produceState(null, packageName) { + withContext(scope.coroutineContext) { + db.getAppPrefsDao().getAppPrefs(packageName).asFlow().collect { value = it } + } + }.value val preferredRepoId = remember(packageName, appPrefs) { appPrefs?.preferredRepoId ?: app.repoId // DB loads preferred repo first, so we remember it } @@ -123,10 +145,11 @@ fun DetailsPresenter( } val authorName = app.authorName val authorHasMoreThanOneApp = if (authorName == null) false else { - val flow = remember(authorName) { - db.getAppDao().hasAuthorMoreThanOneApp(authorName).asFlow() - } - flow.collectAsState(false).value + produceState(false) { + withContext(scope.coroutineContext) { + db.getAppDao().hasAuthorMoreThanOneApp(authorName).asFlow().collect { value = it } + } + }.value } val issue = remember(appsWithIssues) { appsWithIssues?.find { it.packageName == packageName }?.issue diff --git a/next/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt index 77d1552c0..7ce1837f0 100644 --- a/next/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt @@ -48,7 +48,8 @@ class DiscoverViewModel @Inject constructor( ) : AndroidViewModel(app) { private val log = KotlinLogging.logger { } - private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) + private val moleculeScope = + CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) private val collator = Collator.getInstance(Locale.getDefault()) val numUpdates = updatesManager.numUpdates @@ -78,15 +79,17 @@ class DiscoverViewModel @Inject constructor( private val searchResults = MutableStateFlow(null) val localeList = LocaleListCompat.getDefault() - val discoverModel: StateFlow = scope.launchMolecule(mode = ContextClock) { - DiscoverPresenter( - newAppsFlow = newApps, - recentlyUpdatedAppsFlow = recentlyUpdatedApps, - categoriesFlow = categories, - repositoriesFlow = repoManager.repositoriesState, - searchResultsFlow = searchResults, - lastRepoUpdate = settingsManager.lastRepoUpdate, - ) + val discoverModel: StateFlow by lazy(LazyThreadSafetyMode.NONE) { + moleculeScope.launchMolecule(mode = ContextClock) { + DiscoverPresenter( + newAppsFlow = newApps, + recentlyUpdatedAppsFlow = recentlyUpdatedApps, + categoriesFlow = categories, + repositoriesFlow = repoManager.repositoriesState, + searchResultsFlow = searchResults, + lastRepoUpdate = settingsManager.lastRepoUpdate, + ) + } } suspend fun search(term: String) = withContext(ioScope.coroutineContext) { diff --git a/next/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt index 42f0424ea..bbf59e1dd 100644 --- a/next/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/lists/AppListViewModel.kt @@ -43,7 +43,8 @@ class AppListViewModel @Inject constructor( private val onboardingManager: OnboardingManager, ) : AndroidViewModel(app), AppListActions { - private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) + private val moleculeScope = + CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) private val localeList = LocaleListCompat.getDefault() private val apps = MutableStateFlow?>(null) @@ -77,17 +78,19 @@ class AppListViewModel @Inject constructor( private val filteredRepositoryIds = MutableStateFlow>(emptySet()) val showOnboarding = onboardingManager.showFilterOnboarding - val appListModel: StateFlow = scope.launchMolecule(mode = ContextClock) { - AppListPresenter( - appsFlow = apps, - sortByFlow = sortBy, - filterIncompatibleFlow = filterIncompatible, - categoriesFlow = categories, - filteredCategoryIdsFlow = filteredCategoryIds, - repositoriesFlow = repositories, - filteredRepositoryIdsFlow = filteredRepositoryIds, - searchQueryFlow = query, - ) + val appListModel: StateFlow by lazy(LazyThreadSafetyMode.NONE) { + moleculeScope.launchMolecule(mode = ContextClock) { + AppListPresenter( + appsFlow = apps, + sortByFlow = sortBy, + filterIncompatibleFlow = filterIncompatible, + categoriesFlow = categories, + filteredCategoryIdsFlow = filteredCategoryIds, + repositoriesFlow = repositories, + filteredRepositoryIdsFlow = filteredRepositoryIds, + searchQueryFlow = query, + ) + } } @UiThread diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt index 4eddeb9e7..f6d8e6d9f 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt @@ -53,14 +53,16 @@ class RepositoriesViewModel @Inject constructor( } // define below init, because this only defines repoSortingMap - val model: StateFlow = moleculeScope.launchMolecule(mode = ContextClock) { - RepositoriesPresenter( - context = application, - repositoriesFlow = repos, - repoSortingMapFlow = repoSortingMap, - showOnboardingFlow = showOnboarding, - lastUpdateFlow = settingsManager.lastRepoUpdateFlow, - ) + val model: StateFlow by lazy(LazyThreadSafetyMode.NONE) { + moleculeScope.launchMolecule(mode = ContextClock) { + RepositoriesPresenter( + context = application, + repositoriesFlow = repos, + repoSortingMapFlow = repoSortingMap, + showOnboardingFlow = showOnboarding, + lastUpdateFlow = settingsManager.lastRepoUpdateFlow, + ) + } } private fun onRepositoriesChanged(repositories: List) { diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt index a79bf0593..3f8c25f86 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt @@ -56,14 +56,16 @@ class RepoDetailsViewModel @Inject constructor( private val archiveStateFlow = MutableStateFlow(UNKNOWN) private val showOnboarding = onboardingManager.showRepoDetailsOnboarding - val model: StateFlow = moleculeScope.launchMolecule(mode = ContextClock) { - RepoDetailsPresenter( - repoFlow = repoFlow, - numAppsFlow = numAppsFlow, - archiveStateFlow = archiveStateFlow, - showOnboardingFlow = showOnboarding, - proxyConfig = settingsManager.proxyConfig, - ) + val model: StateFlow by lazy(LazyThreadSafetyMode.NONE) { + moleculeScope.launchMolecule(mode = ContextClock) { + RepoDetailsPresenter( + repoFlow = repoFlow, + numAppsFlow = numAppsFlow, + archiveStateFlow = archiveStateFlow, + showOnboardingFlow = showOnboarding, + proxyConfig = settingsManager.proxyConfig, + ) + } } fun setRepoId(repoId: Long) {