diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt index 9f5ebf4ef34..b51df39e16d 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt @@ -24,7 +24,6 @@ import io.homeassistant.companion.android.util.HAWebChromeClient import io.homeassistant.companion.android.util.HAWebViewClient import io.homeassistant.companion.android.util.HAWebViewClientFactory import javax.inject.Inject -import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi import kotlinx.coroutines.Job @@ -35,7 +34,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest @@ -50,9 +48,6 @@ import timber.log.Timber @VisibleForTesting val CONNECTION_TIMEOUT = 10.seconds -/** Delay before stopping shared flows after the last subscriber disconnects. */ -private val SUBSCRIPTION_STOP_DELAY = 500.milliseconds - /** * ViewModel for frontend screen. * @@ -144,12 +139,12 @@ internal class FrontendViewModel @VisibleForTesting constructor( override val urlFlow: StateFlow = _viewState.map { it.url } .distinctUntilChanged() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(SUBSCRIPTION_STOP_DELAY), null) + .stateIn(viewModelScope, SharingStarted.Eagerly, _viewState.value.url) override val errorFlow: StateFlow = _viewState.map { state -> (state as? FrontendViewState.Error)?.error } .distinctUntilChanged() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(SUBSCRIPTION_STOP_DELAY), null) + .stateIn(viewModelScope, SharingStarted.Eagerly, (_viewState.value as? FrontendViewState.Error)?.error) /** Flow of scripts to be evaluated in the WebView. */ val scriptsToEvaluate: Flow = frontendMessageHandler.scriptsToEvaluate() diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt index ebe2340cd24..26457edafeb 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt @@ -275,24 +275,47 @@ class FrontendViewModelTest { } @Nested - inner class ErrorFlow { + inner class DerivedFlows { @Test - fun `Given loading state when error occurs then error flow is updated`() = runTest { + fun `Given url manager returns success then urlFlow value matches viewState url without subscribers`() = runTest { + every { urlManager.serverUrlFlow(any(), any()) } returns flowOf( + UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId), + ) + + val viewModel = createViewModel() + advanceTimeBy(CONNECTION_TIMEOUT - 1.seconds) + + // No collector on urlFlow — value must still be up to date (SharingStarted.Eagerly) + assertEquals(testUrlWithAuth, viewModel.urlFlow.value) + } + + @Test + fun `Given error state then errorFlow value matches viewState error without subscribers`() = runTest { every { urlManager.serverUrlFlow(any(), any()) } returns flowOf( UrlLoadResult.ServerNotFound(serverId), ) val viewModel = createViewModel() + advanceUntilIdle() - // Subscribe to errorFlow to activate the stateIn with WhileSubscribed - val errors = mutableListOf() - val job = backgroundScope.launch { viewModel.errorFlow.collect { errors.add(it) } } + assertTrue(viewModel.errorFlow.value is FrontendConnectionError.UnreachableError) + } + } + @Nested + inner class ErrorFlow { + + @Test + fun `Given loading state when error occurs then error flow is updated`() = runTest { + every { urlManager.serverUrlFlow(any(), any()) } returns flowOf( + UrlLoadResult.ServerNotFound(serverId), + ) + + val viewModel = createViewModel() advanceUntilIdle() - assertTrue(errors.any { it is FrontendConnectionError.UnreachableError }) - job.cancel() + assertTrue(viewModel.errorFlow.value is FrontendConnectionError.UnreachableError) } @Test @@ -301,15 +324,10 @@ class FrontendViewModelTest { every { urlManager.serverUrlFlow(any(), any()) } returns urlResults val viewModel = createViewModel() - - // Subscribe to errorFlow to activate the stateIn with WhileSubscribed - val errors = mutableListOf() - val job = backgroundScope.launch { viewModel.errorFlow.collect { errors.add(it) } } - advanceUntilIdle() // Verify error exists - assertTrue(errors.any { it != null }) + assertTrue(viewModel.errorFlow.value != null) // Setup successful response for retry urlResults.value = UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId) @@ -317,9 +335,8 @@ class FrontendViewModelTest { viewModel.onRetry() advanceTimeBy(CONNECTION_TIMEOUT - 1.seconds) - // Error should be cleared - the last emitted error should be null - assertTrue(errors.last() == null) - job.cancel() + // Error should be cleared + assertEquals(null, viewModel.errorFlow.value) } @Test