Try to fix molecule issues

We were running the presenting on the IoDispatcher and this seemed to have caused random crashes.
This commit is contained in:
Torsten Grote
2025-12-03 11:51:35 -03:00
parent 0a12689bcb
commit 42c60b374e
7 changed files with 135 additions and 96 deletions

View File

@@ -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<MyAppsModel> = moleculeScope.launchMolecule(mode = ContextClock) {
MyAppsPresenter(
appUpdatesFlow = updates,
appInstallStatesFlow = appInstallManager.appInstallStates,
appsWithIssuesFlow = updatesManager.appsWithIssues,
installedAppsFlow = installedAppItems,
searchQueryFlow = searchQuery,
sortOrderFlow = sortOrder,
)
val myAppsModel: StateFlow<MyAppsModel> 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() {

View File

@@ -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<AppInfo?>(null)
private val currentRepoIdFlow = MutableStateFlow<Long?>(null)
private val moleculeScope =
CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main)
val appDetails: StateFlow<AppDetailsItem?> = 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<AppDetailsItem?> 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) {

View File

@@ -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<App?>(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<Repository?>(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<List<AppVersion>?>(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<AppPrefs?>(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

View File

@@ -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<SearchResults?>(null)
val localeList = LocaleListCompat.getDefault()
val discoverModel: StateFlow<DiscoverModel> = scope.launchMolecule(mode = ContextClock) {
DiscoverPresenter(
newAppsFlow = newApps,
recentlyUpdatedAppsFlow = recentlyUpdatedApps,
categoriesFlow = categories,
repositoriesFlow = repoManager.repositoriesState,
searchResultsFlow = searchResults,
lastRepoUpdate = settingsManager.lastRepoUpdate,
)
val discoverModel: StateFlow<DiscoverModel> 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) {

View File

@@ -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<List<AppListItem>?>(null)
@@ -77,17 +78,19 @@ class AppListViewModel @Inject constructor(
private val filteredRepositoryIds = MutableStateFlow<Set<Long>>(emptySet())
val showOnboarding = onboardingManager.showFilterOnboarding
val appListModel: StateFlow<AppListModel> = scope.launchMolecule(mode = ContextClock) {
AppListPresenter(
appsFlow = apps,
sortByFlow = sortBy,
filterIncompatibleFlow = filterIncompatible,
categoriesFlow = categories,
filteredCategoryIdsFlow = filteredCategoryIds,
repositoriesFlow = repositories,
filteredRepositoryIdsFlow = filteredRepositoryIds,
searchQueryFlow = query,
)
val appListModel: StateFlow<AppListModel> 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

View File

@@ -53,14 +53,16 @@ class RepositoriesViewModel @Inject constructor(
}
// define below init, because this only defines repoSortingMap
val model: StateFlow<RepositoryModel> = moleculeScope.launchMolecule(mode = ContextClock) {
RepositoriesPresenter(
context = application,
repositoriesFlow = repos,
repoSortingMapFlow = repoSortingMap,
showOnboardingFlow = showOnboarding,
lastUpdateFlow = settingsManager.lastRepoUpdateFlow,
)
val model: StateFlow<RepositoryModel> 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<Repository>) {

View File

@@ -56,14 +56,16 @@ class RepoDetailsViewModel @Inject constructor(
private val archiveStateFlow = MutableStateFlow(UNKNOWN)
private val showOnboarding = onboardingManager.showRepoDetailsOnboarding
val model: StateFlow<RepoDetailsModel> = moleculeScope.launchMolecule(mode = ContextClock) {
RepoDetailsPresenter(
repoFlow = repoFlow,
numAppsFlow = numAppsFlow,
archiveStateFlow = archiveStateFlow,
showOnboardingFlow = showOnboarding,
proxyConfig = settingsManager.proxyConfig,
)
val model: StateFlow<RepoDetailsModel> by lazy(LazyThreadSafetyMode.NONE) {
moleculeScope.launchMolecule(mode = ContextClock) {
RepoDetailsPresenter(
repoFlow = repoFlow,
numAppsFlow = numAppsFlow,
archiveStateFlow = archiveStateFlow,
showOnboardingFlow = showOnboarding,
proxyConfig = settingsManager.proxyConfig,
)
}
}
fun setRepoId(repoId: Long) {