I am implementing a weather app using Jetpack Compose. It includes a function to choose the temperature unit (Celsius or Fahrenheit). Whenever the temperature unit is changed, the data should refresh.
To achieve this, I used LaunchedEffect. However, the screen data refreshes even when I exit and return from another screen, without changing the unit.
Here is my code.
@Composable
fun HomeScreen(
userPrefs: UserPrefs,
onMenuClicked: () -> Unit,
onNext7DaysClicked: (List<DailyWeather>) -> Unit,
modifier: Modifier = Modifier,
) {
val viewModel = hiltViewModel<HomeViewModel>()
val state by viewModel.state.collectAsState()
LaunchedEffect(userPrefs.tempUnit) {
val selectedTempUnit = TemperatureUnit.entries[userPrefs.tempUnit]
val weatherUnit = if (selectedTempUnit == TemperatureUnit.CELSIUS) WeatherUnit.METRIC else WeatherUnit.IMPERIAL
viewModel.loadWeatherData(weatherUnit)
}
.....
}
ViewModel
@HiltViewModel
class HomeViewModel @Inject constructor(
private val getWeather: GetWeatherUseCase,
private val getCurrentLocation: GetCurrentLocationUseCase,
) : ViewModel() {
data class UIState(
val data: ForecastWeather? = null,
val exception: Exception? = null
)
private val _state = MutableStateFlow(UIState())
val state: StateFlow<UIState> = _state.asStateFlow()
fun loadWeatherData(unit: WeatherUnit) {
viewModelScope.launch {
when (val locResult = getCurrentLocation()) {
is DataResult.Success -> {
getWeatherData(locResult.data, unit)
}
is DataResult.Failed -> {
_state.update { it.copy(exception = locResult.exception) }
}
}
}
}
private suspend fun getWeatherData(location: Location, unit: WeatherUnit) {
when (val result = getWeather.invoke(location, unit)) {
is DataResult.Success -> {
_state.update { it.copy(data = result.data) }
}
is DataResult.Failed -> {
_state.value = UIState(
data = result.data,
exception = result.exception
)
}
}
}
}
From the documentation:
When LaunchedEffect enters the composition it will launch block [...].
When you navigate away from HomeScreen, the LaunchedEffect's block is cancelled, when you navigate back to it, it is restarted. That's how Compose works in general, it executes everything again when it enters the composition. If you want to persist data in between you need to save that data in a view model.
When using Compose it is very important to separate the UI from data retrieval. The UI should only concern itself with displaying the data, not with the logic required to retrieve it. That falls in the responsibility of the view model (which itself may delegate things to the data layer).
For your code that means that loadWeatherData
should never be called from your composables. It seems you want to load the data as soon as the view model is created, so you can call it in the view model's init { }
block. For that you need the WeatherUnit
the user selected, extracted from UserPrefs
. Since UserPrefs seems to hold user preferences that should survive the lifecycle of any composable (probably even persisted in a Preferences DataStore), it belongs in the view model as well.1 The same applies to the logic necessary to derive the actual WeatherUnit
. That is unrelated to the UI.
With all of that removed from the LaunchedEffect it is now empty, so you can remove it altogether. HomeScreen now simply retrieves the necessary data from the view model and displays it. Nothing can go wrong anymore when navigating around in your app, the view model will always remain in the same state, unaffected by what the UI currently displays. That's why it is so important to cleanly separate data retrieval and UI.
1 Wrapped in a Flow so you can trigger another loadWeatherData
when it changes. You can even remove the init
block and base the execution of loadWeatherData
entirely on this new WeatherUnit Flow.