diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..3f61fc2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 79a2989..797fd1e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + { + data class Success(val data: T) : Result + data class Error(val exception: Throwable) : Result + data object Loading : Result +} + +fun Flow.asResult(): Flow> = map> { Result.Success(it) } + .onStart { emit(Result.Loading) } + .catch { emit(Result.Error(it)) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb7272d..a5fc7b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,8 @@ kotlinxSerializationJson = "1.8.1" ksp = "2.1.21-2.0.1" # https://github.com/google/ksp/releases material = "1.12.0" mockk = "1.14.2" # https://github.com/mockk/mockk/releases +moshi = "1.15.2" +moshiKotlinCodegen = "1.15.2" okhttpBom = "4.12.0" retrofit = "3.0.0" 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" } app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" } 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-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } 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" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } 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-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" } okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" } 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" } truth = { module = "com.google.truth:truth", version.ref = "truth" } diff --git a/model/data/api/build.gradle.kts b/model/data/api/build.gradle.kts index 88540a9..47b2efa 100644 --- a/model/data/api/build.gradle.kts +++ b/model/data/api/build.gradle.kts @@ -12,10 +12,12 @@ dependencies { implementation(platform(libs.okhttp.bom)) - implementation(libs.converter.moshi) + implementation(libs.moshi) implementation(libs.okhttp) implementation(libs.okhttp.logging.interceptor) implementation(libs.retrofit) + implementation(libs.retrofit.converter.moshi) + ksp(libs.moshi.kotlin.codegen) implementation(libs.timber) implementation(libs.gson) diff --git a/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/usecases/ObserveAirportsUseCaseImpl.kt b/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/usecases/ObserveAirportsUseCaseImpl.kt index 716aba2..7ef5f4c 100644 --- a/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/usecases/ObserveAirportsUseCaseImpl.kt +++ b/model/repository/src/main/kotlin/dev/adriankuta/flights/model/repository/usecases/ObserveAirportsUseCaseImpl.kt @@ -15,9 +15,9 @@ internal class ObserveAirportsUseCaseImpl @Inject constructor( private val airportsDatasource: AirportsDatasource, ) : ObserveAirportsUseCase { override fun invoke(): Flow?> = loadData( - onCacheInvalidated = { - val response = airportService.getAirports("PL") - airportsDatasource.setAirportsInfo(response, "PL") + onCacheInvalidated = { cacheKey -> + val response = airportService.getAirports("pl") + airportsDatasource.setAirportsInfo(response, cacheKey) }, observeCache = { airportsDatasource.airports diff --git a/ui/home/build.gradle.kts b/ui/home/build.gradle.kts index b1e9d99..d0e1212 100644 --- a/ui/home/build.gradle.kts +++ b/ui/home/build.gradle.kts @@ -9,6 +9,7 @@ android { } dependencies { + implementation(projects.core.util) implementation(projects.ui.designsystem) implementation(projects.domain.search) diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt index dcc9e79..33516e1 100644 --- a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel 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.PreviewDevices @@ -19,18 +18,24 @@ internal fun HomeScreen( val homeUiState by viewModel.uiState.collectAsStateWithLifecycle() HomeScreen( - airports = homeUiState.airports + uiState = homeUiState, ) } @Composable private fun HomeScreen( - airports: List, + uiState: HomeUiState, modifier: Modifier = Modifier, ) { - LazyColumn { - items(airports) { airport -> - Text(airport.name) + LazyColumn( + 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) + } } } } @@ -40,7 +45,7 @@ private fun HomeScreen( private fun HomeScreenPreview() { FlightsTheme { HomeScreen( - airports = listOf() + uiState = HomeUiState.Loading, ) } } diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt index 922394c..4cdead2 100644 --- a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt @@ -3,17 +3,15 @@ package dev.adriankuta.flights.ui.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.types.AirportInfo -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds @HiltViewModel class HomeScreenViewModel @Inject constructor( @@ -26,7 +24,7 @@ class HomeScreenViewModel @Inject constructor( .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = HomeUiState(), + initialValue = HomeUiState.Loading, ) } @@ -34,14 +32,19 @@ private fun homeUiState( useCase: ObserveAirportsUseCase ): Flow { - return flow { - delay(15.seconds) - emit("") - }.flatMapLatest { useCase() }.map { - HomeUiState(it.orEmpty()) - } + return useCase() + .asResult() + .map { result -> + when (result) { + is Result.Error -> HomeUiState.Error(result.exception) + Result.Loading -> HomeUiState.Loading + is Result.Success -> HomeUiState.Success(result.data.orEmpty()) + } + } } -internal data class HomeUiState( - val airports: List = emptyList() -) +internal sealed interface HomeUiState { + data object Loading : HomeUiState + data class Success(val airports: List) : HomeUiState + data class Error(val exception: Throwable) : HomeUiState +}