mirror of
				https://github.com/AdrianKuta/android-challange-adrian-kuta.git
				synced 2025-10-30 23:33:39 +01:00 
			
		
		
		
	feat: Introduce Result type and update UI states
This commit introduces a generic `Result` sealed interface to represent success, error, and loading states for asynchronous operations. It also updates the home screen UI and ViewModel to utilize this `Result` type for displaying different states. Key changes: - Added `Result.kt` in `core:util` defining the `Result` sealed interface and an `asResult()` Flow extension. - Updated `HomeScreenViewModel` to use `asResult()` and map the `ObserveAirportsUseCase` output to `HomeUiState` (Loading, Success, Error). - Modified `HomeUiState` to be a sealed interface with `Loading`, `Success`, and `Error` subtypes. - Updated `HomeScreen` to handle different `HomeUiState` values and display appropriate UI (Loading text, Error text, or list of airports). - Added `core.util` dependency to `ui:home`. - Updated Moshi and Retrofit Moshi converter dependencies in `model/data/api/build.gradle.kts` and `gradle/libs.versions.toml`. - Added `moshi` and `moshiKotlinCodegen` versions to `gradle/libs.versions.toml`. - Removed `converter-moshi` and added `retrofit-converter-moshi` in `gradle/libs.versions.toml`. - Added `ksp(libs.moshi.kotlin.codegen)` to `model/data/api/build.gradle.kts`. - Added `INTERNET` permission to `app/src/main/AndroidManifest.xml`. - Updated `ObserveAirportsUseCaseImpl` to use `cacheKey` when setting airports info and to fetch airports for "pl" (lowercase). - Added `.editorconfig` file with Kotlin trailing comma settings.
This commit is contained in:
		
							
								
								
									
										4
									
								
								.editorconfig
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								.editorconfig
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | [*.{kt,kts}] | ||||||
|  | ij_kotlin_allow_trailing_comma = true | ||||||
|  | ij_kotlin_allow_trailing_comma_on_call_site = true | ||||||
|  |  | ||||||
| @@ -2,6 +2,8 @@ | |||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:tools="http://schemas.android.com/tools"> |     xmlns:tools="http://schemas.android.com/tools"> | ||||||
|  |  | ||||||
|  |     <uses-permission android:name="android.permission.INTERNET" /> | ||||||
|  |  | ||||||
|     <application |     <application | ||||||
|         android:name=".MyApplication" |         android:name=".MyApplication" | ||||||
|         android:allowBackup="true" |         android:allowBackup="true" | ||||||
|   | |||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | package dev.adriankuta.flights.core.util | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.catch | ||||||
|  | import kotlinx.coroutines.flow.map | ||||||
|  | import kotlinx.coroutines.flow.onStart | ||||||
|  |  | ||||||
|  | sealed interface Result<out T> { | ||||||
|  |     data class Success<T>(val data: T) : Result<T> | ||||||
|  |     data class Error(val exception: Throwable) : Result<Nothing> | ||||||
|  |     data object Loading : Result<Nothing> | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T> Flow<T>.asResult(): Flow<Result<T>> = map<T, Result<T>> { Result.Success(it) } | ||||||
|  |     .onStart { emit(Result.Loading) } | ||||||
|  |     .catch { emit(Result.Error(it)) } | ||||||
| @@ -33,6 +33,8 @@ kotlinxSerializationJson = "1.8.1" | |||||||
| ksp = "2.1.21-2.0.1" # https://github.com/google/ksp/releases | ksp = "2.1.21-2.0.1" # https://github.com/google/ksp/releases | ||||||
| material = "1.12.0" | material = "1.12.0" | ||||||
| mockk = "1.14.2" # https://github.com/mockk/mockk/releases | mockk = "1.14.2" # https://github.com/mockk/mockk/releases | ||||||
|  | moshi = "1.15.2" | ||||||
|  | moshiKotlinCodegen = "1.15.2" | ||||||
| okhttpBom = "4.12.0" | okhttpBom = "4.12.0" | ||||||
| retrofit = "3.0.0" | retrofit = "3.0.0" | ||||||
| secrets = "2.0.1" | secrets = "2.0.1" | ||||||
| @@ -73,7 +75,6 @@ androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", | |||||||
| androidx-ui-text-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version.ref = "uiTextGoogleFonts" } | androidx-ui-text-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version.ref = "uiTextGoogleFonts" } | ||||||
| app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" } | app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" } | ||||||
| appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } | appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } | ||||||
| converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshi" } |  | ||||||
| detekt-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } | detekt-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } | ||||||
| detekt-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } | detekt-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } | ||||||
| gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } | gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } | ||||||
| @@ -89,10 +90,13 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version. | |||||||
| kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } | ||||||
| material = { group = "com.google.android.material", name = "material", version.ref = "material" } | material = { group = "com.google.android.material", name = "material", version.ref = "material" } | ||||||
| mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } | mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } | ||||||
|  | moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } | ||||||
|  | moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshiKotlinCodegen" } | ||||||
| okhttp = { module = "com.squareup.okhttp3:okhttp" } | okhttp = { module = "com.squareup.okhttp3:okhttp" } | ||||||
| okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" } | okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" } | ||||||
| okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" } | okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" } | ||||||
| retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } | retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } | ||||||
|  | retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshi" } | ||||||
| timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } | timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } | ||||||
| truth = { module = "com.google.truth:truth", version.ref = "truth" } | truth = { module = "com.google.truth:truth", version.ref = "truth" } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,10 +12,12 @@ dependencies { | |||||||
|  |  | ||||||
|     implementation(platform(libs.okhttp.bom)) |     implementation(platform(libs.okhttp.bom)) | ||||||
|  |  | ||||||
|     implementation(libs.converter.moshi) |     implementation(libs.moshi) | ||||||
|     implementation(libs.okhttp) |     implementation(libs.okhttp) | ||||||
|     implementation(libs.okhttp.logging.interceptor) |     implementation(libs.okhttp.logging.interceptor) | ||||||
|     implementation(libs.retrofit) |     implementation(libs.retrofit) | ||||||
|  |     implementation(libs.retrofit.converter.moshi) | ||||||
|  |     ksp(libs.moshi.kotlin.codegen) | ||||||
|  |  | ||||||
|     implementation(libs.timber) |     implementation(libs.timber) | ||||||
|     implementation(libs.gson) |     implementation(libs.gson) | ||||||
|   | |||||||
| @@ -15,9 +15,9 @@ internal class ObserveAirportsUseCaseImpl @Inject constructor( | |||||||
|     private val airportsDatasource: AirportsDatasource, |     private val airportsDatasource: AirportsDatasource, | ||||||
| ) : ObserveAirportsUseCase { | ) : ObserveAirportsUseCase { | ||||||
|     override fun invoke(): Flow<List<AirportInfo>?> = loadData( |     override fun invoke(): Flow<List<AirportInfo>?> = loadData( | ||||||
|         onCacheInvalidated = { |         onCacheInvalidated = { cacheKey -> | ||||||
|             val response = airportService.getAirports("PL") |             val response = airportService.getAirports("pl") | ||||||
|             airportsDatasource.setAirportsInfo(response, "PL") |             airportsDatasource.setAirportsInfo(response, cacheKey) | ||||||
|         }, |         }, | ||||||
|         observeCache = { |         observeCache = { | ||||||
|             airportsDatasource.airports |             airportsDatasource.airports | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ android { | |||||||
| } | } | ||||||
|  |  | ||||||
| dependencies { | dependencies { | ||||||
|  |     implementation(projects.core.util) | ||||||
|     implementation(projects.ui.designsystem) |     implementation(projects.ui.designsystem) | ||||||
|     implementation(projects.domain.search) |     implementation(projects.domain.search) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ import androidx.compose.runtime.getValue | |||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.hilt.navigation.compose.hiltViewModel | import androidx.hilt.navigation.compose.hiltViewModel | ||||||
| import androidx.lifecycle.compose.collectAsStateWithLifecycle | import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||||||
| import dev.adriankuta.flights.domain.types.AirportInfo |  | ||||||
| import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme | import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme | ||||||
| import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices | import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices | ||||||
|  |  | ||||||
| @@ -19,20 +18,26 @@ internal fun HomeScreen( | |||||||
|     val homeUiState by viewModel.uiState.collectAsStateWithLifecycle() |     val homeUiState by viewModel.uiState.collectAsStateWithLifecycle() | ||||||
|  |  | ||||||
|     HomeScreen( |     HomeScreen( | ||||||
|         airports = homeUiState.airports |         uiState = homeUiState, | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
|  |  | ||||||
| @Composable | @Composable | ||||||
| private fun HomeScreen( | private fun HomeScreen( | ||||||
|     airports: List<AirportInfo>, |     uiState: HomeUiState, | ||||||
|     modifier: Modifier = Modifier, |     modifier: Modifier = Modifier, | ||||||
| ) { | ) { | ||||||
|     LazyColumn { |     LazyColumn( | ||||||
|         items(airports) { airport -> |         modifier = modifier, | ||||||
|  |     ) { | ||||||
|  |         when (uiState) { | ||||||
|  |             is HomeUiState.Error -> item { Text("Error") } | ||||||
|  |             HomeUiState.Loading -> item { Text("Loading") } | ||||||
|  |             is HomeUiState.Success -> items(uiState.airports) { airport -> | ||||||
|                 Text(airport.name) |                 Text(airport.name) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @PreviewDevices | @PreviewDevices | ||||||
| @@ -40,7 +45,7 @@ private fun HomeScreen( | |||||||
| private fun HomeScreenPreview() { | private fun HomeScreenPreview() { | ||||||
|     FlightsTheme { |     FlightsTheme { | ||||||
|         HomeScreen( |         HomeScreen( | ||||||
|             airports = listOf() |             uiState = HomeUiState.Loading, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,17 +3,15 @@ package dev.adriankuta.flights.ui.home | |||||||
| import androidx.lifecycle.ViewModel | import androidx.lifecycle.ViewModel | ||||||
| import androidx.lifecycle.viewModelScope | import androidx.lifecycle.viewModelScope | ||||||
| import dagger.hilt.android.lifecycle.HiltViewModel | import dagger.hilt.android.lifecycle.HiltViewModel | ||||||
|  | import dev.adriankuta.flights.core.util.Result | ||||||
|  | import dev.adriankuta.flights.core.util.asResult | ||||||
| import dev.adriankuta.flights.domain.search.ObserveAirportsUseCase | import dev.adriankuta.flights.domain.search.ObserveAirportsUseCase | ||||||
| import dev.adriankuta.flights.domain.types.AirportInfo | import dev.adriankuta.flights.domain.types.AirportInfo | ||||||
| import kotlinx.coroutines.delay |  | ||||||
| import kotlinx.coroutines.flow.Flow | import kotlinx.coroutines.flow.Flow | ||||||
| import kotlinx.coroutines.flow.SharingStarted | import kotlinx.coroutines.flow.SharingStarted | ||||||
| import kotlinx.coroutines.flow.flatMapLatest |  | ||||||
| import kotlinx.coroutines.flow.flow |  | ||||||
| import kotlinx.coroutines.flow.map | import kotlinx.coroutines.flow.map | ||||||
| import kotlinx.coroutines.flow.stateIn | import kotlinx.coroutines.flow.stateIn | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
| import kotlin.time.Duration.Companion.seconds |  | ||||||
|  |  | ||||||
| @HiltViewModel | @HiltViewModel | ||||||
| class HomeScreenViewModel @Inject constructor( | class HomeScreenViewModel @Inject constructor( | ||||||
| @@ -26,7 +24,7 @@ class HomeScreenViewModel @Inject constructor( | |||||||
|         .stateIn( |         .stateIn( | ||||||
|             scope = viewModelScope, |             scope = viewModelScope, | ||||||
|             started = SharingStarted.WhileSubscribed(5_000), |             started = SharingStarted.WhileSubscribed(5_000), | ||||||
|             initialValue = HomeUiState(), |             initialValue = HomeUiState.Loading, | ||||||
|         ) |         ) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -34,14 +32,19 @@ private fun homeUiState( | |||||||
|     useCase: ObserveAirportsUseCase |     useCase: ObserveAirportsUseCase | ||||||
| ): Flow<HomeUiState> { | ): Flow<HomeUiState> { | ||||||
|  |  | ||||||
|     return flow { |     return useCase() | ||||||
|         delay(15.seconds) |         .asResult() | ||||||
|         emit("") |         .map { result -> | ||||||
|     }.flatMapLatest { useCase() }.map { |             when (result) { | ||||||
|         HomeUiState(it.orEmpty()) |                 is Result.Error -> HomeUiState.Error(result.exception) | ||||||
|  |                 Result.Loading -> HomeUiState.Loading | ||||||
|  |                 is Result.Success -> HomeUiState.Success(result.data.orEmpty()) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
| } | } | ||||||
|  |  | ||||||
| internal data class HomeUiState( | internal sealed interface HomeUiState { | ||||||
|     val airports: List<AirportInfo> = emptyList() |     data object Loading : HomeUiState | ||||||
| ) |     data class Success(val airports: List<AirportInfo>) : HomeUiState | ||||||
|  |     data class Error(val exception: Throwable) : HomeUiState | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user