refactor: replace collectAsState with lifecycle-aware collectAsStateWithLifecycle

Migrate all Flow state collection to collectAsStateWithLifecycle for proper lifecycle awareness, ensuring collection stops when the composable is not in the STARTED state.

- Added lifecycle-runtime-compose dependency to app and app-tv modules
- Replaced all collectAsState() calls with collectAsStateWithLifecycle()
- Updated initial parameter to initialValue for the lifecycle-aware variant
This commit is contained in:
Purefin Dev
2026-05-21 19:47:58 +00:00
parent 645e28ab30
commit 8da759257c
23 changed files with 94 additions and 91 deletions

View File

@@ -58,6 +58,7 @@ dependencies {
implementation(project(":core-ui"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.activity.compose)

View File

@@ -16,8 +16,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -146,7 +146,7 @@ class PurefinActivity : ComponentActivity() {
navigationManager: NavigationManager
) {
var sessionLoaded by remember { mutableStateOf(false) }
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsStateWithLifecycle(initialValue = false)
LaunchedEffect(Unit) {
userSessionRepository.isLoggedIn.collect {

View File

@@ -11,8 +11,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
@@ -38,15 +38,15 @@ fun AppScreen(
updateViewModel: AppUpdateViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) {
val libraries by viewModel.libraries.collectAsState()
val libraryContent by viewModel.latestLibraryContent.collectAsState()
val suggestions by viewModel.suggestions.collectAsState()
val continueWatching by viewModel.continueWatching.collectAsState()
val nextUp by viewModel.nextUp.collectAsState()
val isRefreshing by viewModel.isRefreshing.collectAsState()
val isOnline by viewModel.isOnline.collectAsState()
val isCheckingForUpdates by updateViewModel.isCheckingForUpdates.collectAsState()
val availableUpdate by updateViewModel.availableUpdate.collectAsState()
val libraries by viewModel.libraries.collectAsStateWithLifecycle()
val libraryContent by viewModel.latestLibraryContent.collectAsStateWithLifecycle()
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
val continueWatching by viewModel.continueWatching.collectAsStateWithLifecycle()
val nextUp by viewModel.nextUp.collectAsStateWithLifecycle()
val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()
val isOnline by viewModel.isOnline.collectAsStateWithLifecycle()
val isCheckingForUpdates by updateViewModel.isCheckingForUpdates.collectAsStateWithLifecycle()
val availableUpdate by updateViewModel.availableUpdate.collectAsStateWithLifecycle()
val navigationManager = LocalNavigationManager.current
val snackbarHostState = remember { SnackbarHostState() }

View File

@@ -17,7 +17,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@@ -32,8 +32,8 @@ fun DownloadsContent(
modifier: Modifier = Modifier,
viewModel: DownloadsViewModel = hiltViewModel(),
) {
val downloads = viewModel.downloads.collectAsState(emptyList())
val activeDownloads = viewModel.activeDownloads.collectAsState()
val downloads = viewModel.downloads.collectAsStateWithLifecycle(initialValue = emptyList())
val activeDownloads = viewModel.activeDownloads.collectAsStateWithLifecycle()
val isEmpty = downloads.value.isEmpty() && activeDownloads.value.isEmpty()

View File

@@ -8,7 +8,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@@ -46,8 +46,8 @@ fun EpisodeScreen(
viewModel.selectEpisode(episode)
}
val episode = viewModel.episode.collectAsState()
val downloadState = viewModel.downloadState.collectAsState()
val episode = viewModel.episode.collectAsStateWithLifecycle()
val downloadState = viewModel.downloadState.collectAsStateWithLifecycle()
val notificationPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()

View File

@@ -24,8 +24,8 @@ import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -55,7 +55,7 @@ fun SearchOverlay(
BackHandler(onBack = onDismiss)
var query by rememberSaveable { mutableStateOf("") }
val searchResults by searchViewModel.searchResult.collectAsState()
val searchResults by searchViewModel.searchResult.collectAsStateWithLifecycle()
val dismissInteractionSource = remember { MutableInteractionSource() }
Box(

View File

@@ -14,7 +14,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
@@ -40,7 +40,7 @@ fun LibraryScreen(
viewModel.selectLibrary(libraryId = library.id)
}
val libraryItems = viewModel.contents.collectAsState()
val libraryItems = viewModel.contents.collectAsStateWithLifecycle()
Scaffold(
modifier = modifier.testTag(LibraryScreenTag),

View File

@@ -1,8 +1,8 @@
package hu.bbara.purefin.ui.screen.login
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
@@ -16,18 +16,18 @@ fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) {
val serverUrl by viewModel.url.collectAsState()
val phase by viewModel.phase.collectAsState()
val selectedServer by viewModel.selectedServer.collectAsState()
val discoveredServers by viewModel.discoveredServers.collectAsState()
val username by viewModel.username.collectAsState()
val password by viewModel.password.collectAsState()
val isSearching by viewModel.isSearching.collectAsState()
val isLoggingIn by viewModel.isLoggingIn.collectAsState()
val quickConnectAvailable by viewModel.quickConnectAvailable.collectAsState()
val quickConnectCode by viewModel.quickConnectCode.collectAsState()
val isQuickConnecting by viewModel.isQuickConnecting.collectAsState()
val errorMessage by viewModel.errorMessage.collectAsState()
val serverUrl by viewModel.url.collectAsStateWithLifecycle()
val phase by viewModel.phase.collectAsStateWithLifecycle()
val selectedServer by viewModel.selectedServer.collectAsStateWithLifecycle()
val discoveredServers by viewModel.discoveredServers.collectAsStateWithLifecycle()
val username by viewModel.username.collectAsStateWithLifecycle()
val password by viewModel.password.collectAsStateWithLifecycle()
val isSearching by viewModel.isSearching.collectAsStateWithLifecycle()
val isLoggingIn by viewModel.isLoggingIn.collectAsStateWithLifecycle()
val quickConnectAvailable by viewModel.quickConnectAvailable.collectAsStateWithLifecycle()
val quickConnectCode by viewModel.quickConnectCode.collectAsStateWithLifecycle()
val isQuickConnecting by viewModel.isQuickConnecting.collectAsStateWithLifecycle()
val errorMessage by viewModel.errorMessage.collectAsStateWithLifecycle()
val coroutineScope = rememberCoroutineScope()
val state = remember(

View File

@@ -8,7 +8,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
@@ -39,8 +39,8 @@ fun MovieScreen(
viewModel.selectMovie(movie)
}
val movieItem = viewModel.movie.collectAsState()
val downloadState = viewModel.downloadState.collectAsState()
val movieItem = viewModel.movie.collectAsStateWithLifecycle()
val downloadState = viewModel.downloadState.collectAsStateWithLifecycle()
val notificationPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()

View File

@@ -25,8 +25,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -71,8 +71,8 @@ fun PlayerScreen(
viewModel: PlayerViewModel,
onBack: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
val controlsVisible by viewModel.controlsVisible.collectAsState()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val controlsVisible by viewModel.controlsVisible.collectAsStateWithLifecycle()
val context = LocalContext.current
val activity = context as? Activity

View File

@@ -6,8 +6,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -49,7 +49,7 @@ fun SeriesScreen(
viewModel.selectSeries(series)
}
val seriesState = viewModel.series.collectAsState()
val seriesState = viewModel.series.collectAsStateWithLifecycle()
val seriesData = seriesState.value
if (seriesData != null && seriesData.seasons.isNotEmpty()) {
@@ -58,9 +58,9 @@ fun SeriesScreen(
}
SeriesScreenInternal(
series = seriesData,
seriesDownloadState = viewModel.seriesDownloadState.collectAsState().value,
seasonDownloadState = viewModel.seasonDownloadState.collectAsState().value,
isSmartDownloadEnabled = viewModel.isSmartDownloadEnabled.collectAsState().value,
seriesDownloadState = viewModel.seriesDownloadState.collectAsStateWithLifecycle().value,
seasonDownloadState = viewModel.seasonDownloadState.collectAsStateWithLifecycle().value,
isSmartDownloadEnabled = viewModel.isSmartDownloadEnabled.collectAsStateWithLifecycle().value,
onDownloadOptionSelected = { option, selectedSeason ->
when (option) {
SeriesDownloadOption.SEASON ->

View File

@@ -14,8 +14,8 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -42,7 +42,7 @@ fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) {
val settingGroups by viewModel.settingGroups.collectAsState()
val settingGroups by viewModel.settingGroups.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(viewModel) {
@@ -107,7 +107,7 @@ private fun SettingOptionItem(
) {
when (option) {
is RangeSetting -> {
val value by viewModel.value(option).collectAsState(initial = null)
val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null)
value?.let {
RangeSettingItem(
title = option.title,
@@ -119,7 +119,7 @@ private fun SettingOptionItem(
}
is BooleanSetting -> {
val value by viewModel.value(option).collectAsState(initial = null)
val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null)
value?.let {
BooleanSettingItem(
title = option.title,
@@ -130,7 +130,7 @@ private fun SettingOptionItem(
}
is StringSetting -> {
val value by viewModel.value(option).collectAsState(initial = null)
val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null)
value?.let {
StringSettingItem(
title = option.title,
@@ -168,7 +168,7 @@ private fun <T> DropdownSettingOptionItem(
option: DropdownSetting<T>,
viewModel: SettingsViewModel
) {
val value by viewModel.value(option).collectAsState(initial = null)
val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null)
value?.let {
DropdownSettingItem(
title = option.title,