mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-07-01 20:28:00 +02: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