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

@@ -18,8 +18,8 @@ import androidx.compose.material3.TextButton
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
@@ -147,8 +147,8 @@ class TvActivity : ComponentActivity() {
updateViewModel: AppUpdateViewModel = hiltViewModel()
) {
var sessionLoaded by remember { mutableStateOf(false) }
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsState(initial = false)
val availableUpdate by updateViewModel.availableUpdate.collectAsState()
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsStateWithLifecycle(initialValue = false)
val availableUpdate by updateViewModel.availableUpdate.collectAsStateWithLifecycle()
val context = LocalContext.current
LaunchedEffect(Unit) {

View File

@@ -7,8 +7,8 @@ import androidx.compose.material.icons.outlined.Movie
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Tv
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.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
@@ -35,10 +35,10 @@ fun TvAppScreen(
viewModel: AppViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) {
val libraries by viewModel.libraries.collectAsState()
val continueWatching by viewModel.continueWatching.collectAsState()
val nextUp by viewModel.nextUp.collectAsState()
val latestLibraryContent by viewModel.latestLibraryContent.collectAsState()
val libraries by viewModel.libraries.collectAsStateWithLifecycle()
val continueWatching by viewModel.continueWatching.collectAsStateWithLifecycle()
val nextUp by viewModel.nextUp.collectAsStateWithLifecycle()
val latestLibraryContent by viewModel.latestLibraryContent.collectAsStateWithLifecycle()
val navigationManager = LocalNavigationManager.current
@Suppress("UNCHECKED_CAST")

View File

@@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.runtime.withFrameNanos
import androidx.compose.ui.Modifier
@@ -40,8 +40,8 @@ fun TvEpisodeScreen(
viewModel.selectEpisode(episode)
}
val episode = viewModel.episode.collectAsState()
val seriesTitle = viewModel.seriesTitle.collectAsState()
val episode = viewModel.episode.collectAsStateWithLifecycle()
val seriesTitle = viewModel.seriesTitle.collectAsStateWithLifecycle()
val selectedEpisode = episode.value
if (selectedEpisode == null) {

View File

@@ -15,7 +15,7 @@ import androidx.compose.material3.Scaffold
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.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -35,7 +35,7 @@ fun TvLibraryScreen(
viewModel.selectLibrary(libraryId = library.id)
}
val libraryItems = viewModel.contents.collectAsState()
val libraryItems = viewModel.contents.collectAsStateWithLifecycle()
Scaffold(
modifier = modifier,

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 TvLoginScreen(
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

@@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.runtime.withFrameNanos
import androidx.compose.ui.Modifier
@@ -32,7 +32,7 @@ fun TvMovieScreen(
viewModel.selectMovie(movie)
}
val movieItem = viewModel.movie.collectAsState()
val movieItem = viewModel.movie.collectAsStateWithLifecycle()
if (movieItem.value != null) {
TvMovieScreenContent(

View File

@@ -29,8 +29,8 @@ import androidx.compose.material3.MaterialTheme
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
@@ -85,8 +85,8 @@ fun TvPlayerScreen(
viewModel.loadMedia(mediaId)
}
val uiState by viewModel.uiState.collectAsState()
val controlsVisible by viewModel.controlsVisible.collectAsState()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val controlsVisible by viewModel.controlsVisible.collectAsStateWithLifecycle()
var isPlaylistExpanded by remember { mutableStateOf(false) }
var trackPanelType by remember { mutableStateOf<TvTrackPanelType?>(null) }
var pendingTrackButtonFocus by remember { mutableStateOf<TvTrackPanelType?>(null) }

View File

@@ -8,8 +8,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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
@@ -46,7 +46,7 @@ fun TvSeriesScreen(
viewModel.selectSeries(series)
}
val series = viewModel.series.collectAsState()
val series = viewModel.series.collectAsStateWithLifecycle()
val seriesData = series.value
if (seriesData != null && seriesData.seasons.isNotEmpty()) {

View File

@@ -26,8 +26,8 @@ import androidx.compose.material3.Switch
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
@@ -53,7 +53,7 @@ fun TvSettingsScreen(
onBack: () -> Unit = viewModel::onBack,
modifier: Modifier = Modifier
) {
val settingGroups by viewModel.settingGroups.collectAsState()
val settingGroups by viewModel.settingGroups.collectAsStateWithLifecycle()
val context = LocalContext.current
LaunchedEffect(viewModel, context) {
@@ -117,7 +117,7 @@ private fun TvSettingOptionItem(
) {
when (option) {
is RangeSetting -> {
val value by viewModel.value(option).collectAsState(initial = null)
val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null)
value?.let {
TvNumberSettingItem(
title = option.title,
@@ -129,7 +129,7 @@ private fun TvSettingOptionItem(
}
is BooleanSetting -> {
val value by viewModel.value(option).collectAsState(initial = null)
val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null)
value?.let {
TvBooleanSettingItem(
title = option.title,
@@ -140,7 +140,7 @@ private fun TvSettingOptionItem(
}
is StringSetting -> {
val value by viewModel.value(option).collectAsState(initial = null)
val value by viewModel.value(option).collectAsStateWithLifecycle(initialValue = null)
value?.let {
TvStringSettingItem(
title = option.title,
@@ -178,7 +178,7 @@ private fun <T> TvDropdownSettingOptionItem(
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 {
TvDropdownSettingItem(
title = option.title,