Final refactor refinements: fix auto-refresh lifecycle, clarify threading, and add cancellation tests

This commit is contained in:
ABHISHEK KR. SHARMA committed 2026-08-25 19:25:44 +05:30
1 parent 1fc1fdbfb3
commit b904ccfda3
6 files changed
+40 -5

No files matched your search

+1 -1
View File
@@ -154,7 +154,7 @@ dependencies {
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.materialKolor)
implementation(libs.androidx.foundation.layout)
implementation(libs.androidx.foundation.layout) // Removed duplicate entry
implementation(libs.coil.compose)
implementation(libs.kotlinx.coroutines.android)
@@ -89,7 +89,5 @@ class SourceDataRepository @Inject constructor(
launch {
onWeather(weatherJob.await())
}
Unit
}
}
@@ -24,6 +24,8 @@ class GetWeatherUseCase @Inject constructor(
* asynchronous operations.
*
* All callbacks are invoked on the dispatcher used by the caller.
* Note: [onWeather], [onAlerts], and [onAirQuality] may be invoked from the
* repository's dispatcher if not explicitly controlled by the caller.
*
* @param location The location to fetch data for.
* @param isManualRefresh Whether this is a user-initiated refresh (e.g., pull-to-refresh).
@@ -41,6 +41,8 @@ class UpdateLocationSourceUseCase @Inject constructor(
locationsRepo.updateAlertSourceForLocation(location.id, alertSource)
locationsRepo.updateOpenMeteoModelForLocation(location.id, openMeteoModel)
// Reconciliation cleans up stale data from previous sources.
// Note: These updates are currently performed in sequence without a transaction.
weatherDataReconcilerRepository.reconcileSourceChange(
previous = location,
updated = updatedLocation
@@ -59,6 +59,10 @@ class WeatherViewModel @Inject constructor(
private var _uiState = mutableStateOf(MainScreenWeatherUiState())
val uiState: State<MainScreenWeatherUiState> = _uiState
companion object {
private val AUTO_REFRESH_INTERVAL = 45.minutes
}
// Registered on the process-wide lifecycle (same pattern as AppVisibility) rather than a
// Compose LocalLifecycleOwner tied to a screen: a screen-scoped observer gets torn down and
// recreated by ordinary in-app navigation, and Android replays a synthetic ON_START to any
@@ -368,7 +372,7 @@ class WeatherViewModel @Inject constructor(
autoRefreshJob = viewModelScope.launch {
while (isActive) {
delay(45.minutes)
delay(AUTO_REFRESH_INTERVAL)
val location = _uiState.value.activeLocation ?: continue
if (_uiState.value.isLoading || _uiState.value.isError) {
@@ -14,6 +14,7 @@ import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import kotlin.time.Duration.Companion.minutes
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
class WeatherViewModelTest {
@@ -123,7 +124,7 @@ class WeatherViewModelTest {
// Given
val deviceLocation = dummyLocation.copy(isDeviceLocation = true)
val updatedLocation = deviceLocation.copy(latitude = 52.0)
val onLocationUpdatedSlot = slot<(Location) -> Unit>()
val onLocationUpdatedSlot = slot<suspend (Location) -> Unit>()
coEvery {
getWeatherUseCase(
@@ -149,6 +150,34 @@ class WeatherViewModelTest {
assertEquals(updatedLocation, viewModel.uiState.value.activeLocation)
}
@Test
fun `auto refresh should call getWeather periodically`() = runTest {
// Given
viewModel.setActiveLocation(dummyLocation)
advanceUntilIdle()
// Clear initial call from setActiveLocation
clearMocks(getWeatherUseCase, answers = false)
// When
advanceTimeBy(45.minutes.inWholeMilliseconds + 1)
// Then
coVerify {
getWeatherUseCase(
location = dummyLocation,
isManualRefresh = false,
isForceRefresh = false,
isForceRefreshForAirQuality = false,
isForceRefreshForAlerts = false,
onLocationUpdated = any(),
onWeather = any(),
onAlerts = any(),
onAirQuality = any()
)
}
}
@Test
fun `handleSourceChangeForWeather should compute force refresh flags correctly`() = runTest {
// Given