From 929303892a9327f4aaf48ac6d1d64cb0e4d1a46e Mon Sep 17 00:00:00 2001 From: odaridavid Date: Fri, 31 May 2024 16:06:15 +0200 Subject: [PATCH] refactor state for type safety --- .../odaridavid/weatherapp/MainViewModel.kt | 52 ++++++++++-- .../odaridavid/weatherapp/ui/MainActivity.kt | 52 ++++++------ .../weatherapp/ui/home/HomeScreen.kt | 82 ++++++++++--------- .../weatherapp/ui/home/HomeViewModel.kt | 78 ++++++++++++------ 4 files changed, 165 insertions(+), 99 deletions(-) diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt b/app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt index 397acf4..a41aaaa 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt @@ -18,7 +18,7 @@ class MainViewModel @Inject constructor( private val logger: Logger ) : ViewModel() { - private val _state = MutableStateFlow(MainViewState()) + private val _state: MutableStateFlow = MutableStateFlow(MainViewState.Loading) val state: StateFlow = _state.asStateFlow() private val _hasAppUpdate = MutableStateFlow(false) @@ -27,11 +27,15 @@ class MainViewModel @Inject constructor( fun processIntent(mainViewIntent: MainViewIntent) { when (mainViewIntent) { is MainViewIntent.GrantPermission -> { - setState { copy(isPermissionGranted = mainViewIntent.isGranted) } + setState { + toSuccessState().copy(isPermissionGranted = mainViewIntent.isGranted) + } } is MainViewIntent.CheckLocationSettings -> { - setState { copy(isLocationSettingEnabled = mainViewIntent.isEnabled) } + setState { + toSuccessState().copy(isLocationSettingEnabled = mainViewIntent.isEnabled) + } } is MainViewIntent.ReceiveLocation -> { @@ -42,11 +46,14 @@ class MainViewModel @Inject constructor( viewModelScope.launch { settingsRepository.setDefaultLocation(defaultLocation) } - setState { copy(defaultLocation = defaultLocation) } + setState { + toSuccessState().copy(defaultLocation = defaultLocation) + } } is MainViewIntent.LogException -> { logger.logException(mainViewIntent.throwable) + setState { MainViewState.Error } } is MainViewIntent.UpdateApp -> { @@ -62,13 +69,40 @@ class MainViewModel @Inject constructor( _state.emit(stateReducer(state.value)) } } + + // TODO Fix default state on success + private fun MainViewState.toSuccessState( + isPermissionGranted: Boolean = false, + isLocationSettingEnabled: Boolean = false, + defaultLocation: DefaultLocation? = DefaultLocation(longitude = 0.0, latitude = 0.0), + ): MainViewState.Success { + return when (this) { + is MainViewState.Success -> this.copy( + isPermissionGranted = if (this.isPermissionGranted != isPermissionGranted) isPermissionGranted else this.isPermissionGranted, + isLocationSettingEnabled = if (this.isLocationSettingEnabled != isLocationSettingEnabled) isLocationSettingEnabled else this.isLocationSettingEnabled, + defaultLocation = defaultLocation + ) + is MainViewState.Loading, is MainViewState.Error -> MainViewState.Success( + isPermissionGranted = isPermissionGranted, + isLocationSettingEnabled = isLocationSettingEnabled, + defaultLocation = DefaultLocation(longitude = 0.0, latitude = 0.0) + ) + } + } } -data class MainViewState( - val isPermissionGranted: Boolean = false, - val isLocationSettingEnabled: Boolean = false, - val defaultLocation: DefaultLocation? = DefaultLocation(longitude = 0.0, latitude = 0.0) -) +sealed class MainViewState { + + object Loading : MainViewState() + data class Success( + val isPermissionGranted: Boolean, + val isLocationSettingEnabled: Boolean, + val defaultLocation: DefaultLocation? = DefaultLocation(longitude = 0.0, latitude = 0.0) + ) : MainViewState() + + object Error : MainViewState() + +} sealed class MainViewIntent { diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt index cb1bcaa..806aa9b 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt @@ -94,7 +94,9 @@ class MainActivity : ComponentActivity() { WeatherAppTheme { Surface( - modifier = Modifier.fillMaxSize().safeDrawingPadding(), + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), color = MaterialTheme.colorScheme.background ) { val state = mainViewModel.state.collectAsState().value @@ -131,33 +133,33 @@ class MainActivity : ComponentActivity() { @SuppressLint("MissingPermission") @Composable private fun InitMainScreen(state: MainViewState) { - when { - state.isLocationSettingEnabled && state.isPermissionGranted -> { - fusedLocationProviderClient.lastLocation - .addOnSuccessListener { location: Location? -> - location?.run { - mainViewModel.processIntent( - MainViewIntent.ReceiveLocation( - longitude = location.longitude, - latitude = location.latitude + when (state) { + is MainViewState.Loading -> LoadingScreen() + is MainViewState.Error -> { + // TODO show error screen + } + is MainViewState.Success -> { + if (state.isLocationSettingEnabled && !state.isPermissionGranted) { + RequiresPermissionsScreen() + } else if (!state.isLocationSettingEnabled && !state.isPermissionGranted) { + EnableLocationSettingScreen() + } else { + fusedLocationProviderClient.lastLocation + .addOnSuccessListener { location: Location? -> + location?.run { + mainViewModel.processIntent( + MainViewIntent.ReceiveLocation( + longitude = location.longitude, + latitude = location.latitude + ) ) - ) + } + }.addOnFailureListener { exception -> + mainViewModel.processIntent(MainViewIntent.LogException(throwable = exception)) } - }.addOnFailureListener { exception -> - mainViewModel.processIntent(MainViewIntent.LogException(throwable = exception)) - } - WeatherAppScreensConfig(navController = rememberNavController()) - } - - state.isLocationSettingEnabled && !state.isPermissionGranted -> { - RequiresPermissionsScreen() - } - - !state.isLocationSettingEnabled && !state.isPermissionGranted -> { - EnableLocationSettingScreen() + WeatherAppScreensConfig(navController = rememberNavController()) + } } - - else -> LoadingScreen() } } diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeScreen.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeScreen.kt index 012f76f..4c51c27 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeScreen.kt @@ -42,48 +42,50 @@ fun HomeScreen( ) { Column(modifier = Modifier.fillMaxSize()) { - LocalContext.current.getCityName( - latitude = state.defaultLocation.latitude, - longitude = state.defaultLocation.longitude - ) { address -> - val cityName = address.locality - onCityNameReceived(cityName) - } - - HomeTopBar(cityName = state.locationName, onSettingClicked) - - if (state.isLoading) { - LoadingScreen() - } - - if (state.errorMessageId != null) { - ErrorScreen(state.errorMessageId, onTryAgainClicked) - } else { - state.weather?.current?.let { currentWeather -> - CurrentWeatherWidget(currentWeather = currentWeather) - } ?: run { - EmptySectionWidget( - label = stringResource(id = R.string.home_title_currently), - weatherType = stringResource(id = R.string.home_weather_type_currently) - ) + when (state) { + is HomeScreenViewState.Loading -> { + LoadingScreen() } - - state.weather?.hourly?.let { hourlyWeather -> - HourlyWeatherWidget(hourlyWeatherList = hourlyWeather) - } ?: run { - EmptySectionWidget( - label = stringResource(id = R.string.home_today_forecast_title), - weatherType = stringResource(id = R.string.home_weather_type_hourly) - ) + is HomeScreenViewState.Error -> { + ErrorScreen(state.errorMessageId, onTryAgainClicked) } - - state.weather?.daily?.let { dailyWeather -> - DailyWeatherWidget(dailyWeatherList = dailyWeather) - } ?: run { - EmptySectionWidget( - label = stringResource(id = R.string.home_weekly_forecast_title), - weatherType = stringResource(id = R.string.home_weather_type_daily) - ) + is HomeScreenViewState.Success -> { + LocalContext.current.getCityName( + latitude = state.defaultLocation.latitude, + longitude = state.defaultLocation.longitude + ) { address -> + val cityName = address.locality + onCityNameReceived(cityName) + } + + HomeTopBar(cityName = state.locationName, onSettingClicked) + + state.weather.current?.let { currentWeather -> + CurrentWeatherWidget(currentWeather = currentWeather) + } ?: run { + EmptySectionWidget( + label = stringResource(id = R.string.home_title_currently), + weatherType = stringResource(id = R.string.home_weather_type_currently) + ) + } + + state.weather.hourly?.let { hourlyWeather -> + HourlyWeatherWidget(hourlyWeatherList = hourlyWeather) + } ?: run { + EmptySectionWidget( + label = stringResource(id = R.string.home_today_forecast_title), + weatherType = stringResource(id = R.string.home_weather_type_hourly) + ) + } + + state.weather.daily?.let { dailyWeather -> + DailyWeatherWidget(dailyWeatherList = dailyWeather) + } ?: run { + EmptySectionWidget( + label = stringResource(id = R.string.home_weekly_forecast_title), + weatherType = stringResource(id = R.string.home_weather_type_daily) + ) + } } } } diff --git a/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeViewModel.kt b/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeViewModel.kt index ed79788..bf56d9f 100644 --- a/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeViewModel.kt @@ -24,7 +24,7 @@ class HomeViewModel @Inject constructor( private val settingsRepository: SettingsRepository ) : ViewModel() { - private val _state = MutableStateFlow(HomeScreenViewState(isLoading = true)) + private val _state = MutableStateFlow(HomeScreenViewState.Loading as HomeScreenViewState) val state: StateFlow = _state.asStateFlow() init { @@ -37,12 +37,14 @@ class HomeViewModel @Inject constructor( Triple(language, units, defaultLocation) }.collect { (language, units, defaultLocation) -> setState { - copy( + toSuccessState( language = language, units = units, defaultLocation = defaultLocation ) - }.also { processIntent(HomeScreenIntent.LoadWeatherData) } + } + + processIntent(HomeScreenIntent.LoadWeatherData) } } } @@ -52,16 +54,18 @@ class HomeViewModel @Inject constructor( is HomeScreenIntent.LoadWeatherData -> { viewModelScope.launch { val result = weatherRepository.fetchWeatherData( - language = state.value.language.languageValue, - defaultLocation = state.value.defaultLocation, - units = state.value.units.value + language = (state.value as HomeScreenViewState.Success).language.languageValue, + defaultLocation = (state.value as HomeScreenViewState.Success).defaultLocation, + units = (state.value as HomeScreenViewState.Success).units.value ) processResult(result) } } is HomeScreenIntent.DisplayCityName -> { - setState { copy(locationName = homeScreenIntent.cityName) } + setState { + toSuccessState(locationName = homeScreenIntent.cityName) + } } } } @@ -71,20 +75,13 @@ class HomeViewModel @Inject constructor( is Result.Success -> { val weatherData = result.data setState { - copy( - weather = weatherData, - isLoading = false, - errorMessageId = null - ) + toSuccessState(weather = weatherData) } } is Result.Error -> { setState { - copy( - isLoading = false, - errorMessageId = result.errorType.toResourceId() - ) + HomeScreenViewState.Error(errorMessageId = result.errorType.toResourceId()) } } } @@ -95,14 +92,45 @@ class HomeViewModel @Inject constructor( _state.emit(stateReducer(state.value)) } } + + // TODO Fix dissapeared name on the top bar + private fun HomeScreenViewState.toSuccessState( + weather: Weather? = null, + units: Units = Units.METRIC, + locationName: String = "", + language: SupportedLanguage = SupportedLanguage.ENGLISH, + defaultLocation: DefaultLocation = DefaultLocation(0.0, 0.0), + ): HomeScreenViewState.Success { + return when (this) { + is HomeScreenViewState.Success -> this.copy( + units = if (this.units != units) units else this.units, + defaultLocation = if (this.defaultLocation != defaultLocation) defaultLocation else this.defaultLocation, + locationName = if (this.locationName != locationName) locationName else this.locationName, + language = if (this.language != language) language else this.language, + weather = weather ?: this.weather + ) + + is HomeScreenViewState.Loading, is HomeScreenViewState.Error -> HomeScreenViewState.Success( + units = units, + defaultLocation = defaultLocation, + locationName = locationName, + language = language, + weather = weather ?: Weather(null, null, null) + ) + } + } } -data class HomeScreenViewState( - val units: Units = Units.METRIC, - val defaultLocation: DefaultLocation = DefaultLocation(0.0, 0.0), - val locationName: String = "-", - val language: SupportedLanguage = SupportedLanguage.ENGLISH, - val weather: Weather? = null, - val isLoading: Boolean = false, - @StringRes val errorMessageId: Int? = null -) +sealed class HomeScreenViewState { + data class Success( + val units: Units, + val defaultLocation: DefaultLocation = DefaultLocation(0.0, 0.0), + val locationName: String, + val language: SupportedLanguage, + val weather: Weather + ) : HomeScreenViewState() + + object Loading : HomeScreenViewState() + + data class Error(@StringRes val errorMessageId: Int) : HomeScreenViewState() +}