mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-07-01 20:17:58 +02:00
feat: Implement airport data fetching and display
This commit introduces the functionality to fetch airport data from the Ryanair API and display it in the UI. Key changes: - Added `AirportInfoModel` interface and its implementation `AirportInfoModelImpl` to represent airport data in the data layer. - Created `AirportInfoModelMapper` to map `AirportResponse` to `AirportInfoModel`. - Set the base URL for Retrofit in `NetworkModule` to `https://services-api.ryanair.com`. - Added new Gradle modules: `model:data:shared`, `model:datasource:airports`, `model:datasource:shared`, and `domain:search`. - Implemented `CacheImpl` in `model:data:shared` for generic caching. - Defined `ObserveAirportsUseCase` interface in `domain:search` to observe airport data. - Added Detekt configuration for the `domain:search` and `model:datasource:shared` modules. - Created `AirportsDatasource` interface and its implementation `AirportsDatasourceImpl` to manage airport data. - Implemented `AirportInfoDomainMapper` to map `AirportInfoModel` to the domain `AirportInfo`. - Updated `HomeScreen` to display a list of airports using `LazyColumn`. - Updated `HomeScreenViewModel` to fetch and expose airport data via `ObserveAirportsUseCase`. - Added `ObserveAirportsUseCaseModule` to provide the `ObserveAirportsUseCase` implementation. - Implemented `ObserveAirportsUseCaseImpl` in the repository layer to fetch data from the API and update the datasource. - Added `kotlinx-datetime` dependency. - Created `AirportsDatasourceModule` to provide `AirportsDatasource` and its implementation. - Added `CacheObservers.kt` with a utility function `loadData` to handle cache observation and data loading logic. - Updated dependencies in `model:data:simple`, `model:repository`, and `ui:home` build files. - Updated `settings.gradle.kts` to include new modules. - Defined `Cache` interface in `model:datasource:shared`.
This commit is contained in:
13
domain/search/build.gradle.kts
Normal file
13
domain/search/build.gradle.kts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.flights.android.library)
|
||||||
|
alias(libs.plugins.flights.android.library.hilt)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "dev.adriankuta.flights.domain.search"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(projects.domain.types)
|
||||||
|
implementation(libs.timber)
|
||||||
|
}
|
10
domain/search/config/detekt/detekt.yml
Normal file
10
domain/search/config/detekt/detekt.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Deviations from defaults
|
||||||
|
formatting:
|
||||||
|
TrailingCommaOnCallSite:
|
||||||
|
active: true
|
||||||
|
autoCorrect: true
|
||||||
|
useTrailingCommaOnCallSite: true
|
||||||
|
TrailingCommaOnDeclarationSite:
|
||||||
|
active: true
|
||||||
|
autoCorrect: true
|
||||||
|
useTrailingCommaOnDeclarationSite: true
|
@ -0,0 +1,8 @@
|
|||||||
|
package dev.adriankuta.flights.domain.search
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
fun interface ObserveAirportsUseCase {
|
||||||
|
operator fun invoke(): Flow<List<AirportInfo>?>
|
||||||
|
}
|
@ -18,6 +18,7 @@ appUpdateKtx = "2.1.0"
|
|||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
converterMoshi = "2.11.0"
|
converterMoshi = "2.11.0"
|
||||||
coreTest = "1.6.1" # https://developer.android.com/jetpack/androidx/releases/test
|
coreTest = "1.6.1" # https://developer.android.com/jetpack/androidx/releases/test
|
||||||
|
datetime = "0.6.1" # https://github.com/Kotlin/kotlinx-datetime/releases
|
||||||
detekt = "1.23.8" # https://detekt.dev/changelog
|
detekt = "1.23.8" # https://detekt.dev/changelog
|
||||||
detektCompose = "0.4.22" # https://github.com/mrmans0n/compose-rules/releases
|
detektCompose = "0.4.22" # https://github.com/mrmans0n/compose-rules/releases
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
@ -84,6 +85,7 @@ kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.re
|
|||||||
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "immutableCollections" }
|
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "immutableCollections" }
|
||||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesAndroid" }
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesAndroid" }
|
||||||
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
|
||||||
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" }
|
||||||
|
@ -39,6 +39,7 @@ class NetworkModule {
|
|||||||
@Provides
|
@Provides
|
||||||
fun provideRetrofit(client: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder()
|
fun provideRetrofit(client: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder()
|
||||||
// TODO("add URL provided in the task instructions")
|
// TODO("add URL provided in the task instructions")
|
||||||
|
.baseUrl("https://services-api.ryanair.com")
|
||||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
.client(client)
|
.client(client)
|
||||||
.build()
|
.build()
|
||||||
|
14
model/data/shared/build.gradle.kts
Normal file
14
model/data/shared/build.gradle.kts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.flights.android.library)
|
||||||
|
alias(libs.plugins.flights.android.library.hilt)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "dev.adriankuta.flights.model.data.shared"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.model.datasource.shared)
|
||||||
|
|
||||||
|
implementation(libs.timber)
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package dev.adriankuta.flights.model.data.shared
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.model.datasource.shared.Cache
|
||||||
|
|
||||||
|
public data class CacheImpl<T>(override val cacheKey: String?, override val data: T?) :
|
||||||
|
Cache<T> {
|
||||||
|
public companion object {
|
||||||
|
public inline fun <reified T> emptyCache(): CacheImpl<T> =
|
||||||
|
CacheImpl(null, null)
|
||||||
|
|
||||||
|
public inline fun <reified T> Cache<T>?.orEmpty(): Cache<T> =
|
||||||
|
this ?: emptyCache()
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,9 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.core.util)
|
implementation(projects.core.util)
|
||||||
|
implementation(projects.model.data.api)
|
||||||
|
implementation(projects.model.data.shared)
|
||||||
|
implementation(projects.model.datasource.airports)
|
||||||
|
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
implementation(libs.gson)
|
implementation(libs.gson)
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package dev.adriankuta.flights.model.data.simple
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.model.data.shared.CacheImpl
|
||||||
|
import dev.adriankuta.flights.model.data.simple.mappers.toModel
|
||||||
|
import dev.adriankuta.flights.model.datasource.airports.AirportsDatasource
|
||||||
|
import dev.adriankuta.flights.model.datasource.airports.entities.AirportInfoModel
|
||||||
|
import dev.adriankuta.flights.model.datasource.shared.Cache
|
||||||
|
import dev.adriankuta.model.data.api.entities.AirportResponse
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
internal class AirportsDatasourceImpl : AirportsDatasource {
|
||||||
|
|
||||||
|
private val _airports = MutableStateFlow(CacheImpl.emptyCache<List<AirportInfoModel>>())
|
||||||
|
|
||||||
|
override val airports: StateFlow<Cache<List<AirportInfoModel>>> = _airports.asStateFlow()
|
||||||
|
|
||||||
|
override suspend fun setAirportsInfo(
|
||||||
|
airports: List<AirportResponse>,
|
||||||
|
cacheKey: String
|
||||||
|
) {
|
||||||
|
_airports.value = CacheImpl(
|
||||||
|
cacheKey = cacheKey,
|
||||||
|
data = airports.map { it.toModel() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package dev.adriankuta.flights.model.data.simple.di
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import dev.adriankuta.flights.model.data.simple.AirportsDatasourceImpl
|
||||||
|
import dev.adriankuta.flights.model.datasource.airports.AirportsDatasource
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
@Suppress("UnnecessaryAbstractClass")
|
||||||
|
internal abstract class AirportsDatasourceModule {
|
||||||
|
@Binds
|
||||||
|
abstract fun bindApi(impl: AirportsDatasourceImpl): AirportsDatasource
|
||||||
|
|
||||||
|
/*@Binds
|
||||||
|
@IntoSet
|
||||||
|
abstract fun bindReset(impl: RolesDatasourceImpl): ClearDatasource*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal class AirportsDatasourceImplModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provide(): AirportsDatasourceImpl = AirportsDatasourceImpl()
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package dev.adriankuta.flights.model.data.simple.mappers
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.model.datasource.airports.entities.AirportInfoModel
|
||||||
|
import dev.adriankuta.model.data.api.entities.AirportResponse
|
||||||
|
|
||||||
|
internal fun AirportResponse.toModel(): AirportInfoModel {
|
||||||
|
return AirportInfoModelImpl(
|
||||||
|
code = code.orEmpty(),
|
||||||
|
name = name.orEmpty(),
|
||||||
|
seoName = seoName.orEmpty(),
|
||||||
|
isBase = isBase ?: false,
|
||||||
|
timeZone = timeZone.orEmpty(),
|
||||||
|
cityCode = city?.code.orEmpty(),
|
||||||
|
cityName = city?.name.orEmpty(),
|
||||||
|
macCode = macCity?.macCode.orEmpty(),
|
||||||
|
regionCode = region?.code.orEmpty(),
|
||||||
|
regionName = region?.name.orEmpty(),
|
||||||
|
countryCode = country?.code.orEmpty(),
|
||||||
|
countryName = country?.name.orEmpty(),
|
||||||
|
countryCurrencyCode = country?.currencyCode.orEmpty(),
|
||||||
|
latitude = coordinates?.latitude ?: 0.0,
|
||||||
|
longitude = coordinates?.longitude ?: 0.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AirportInfoModelImpl(
|
||||||
|
override val code: String,
|
||||||
|
override val name: String,
|
||||||
|
override val seoName: String,
|
||||||
|
override val isBase: Boolean,
|
||||||
|
override val timeZone: String,
|
||||||
|
override val cityCode: String,
|
||||||
|
override val cityName: String,
|
||||||
|
override val macCode: String,
|
||||||
|
override val regionCode: String,
|
||||||
|
override val regionName: String,
|
||||||
|
override val countryCode: String,
|
||||||
|
override val countryName: String,
|
||||||
|
override val countryCurrencyCode: String,
|
||||||
|
override val latitude: Double,
|
||||||
|
override val longitude: Double
|
||||||
|
) : AirportInfoModel
|
15
model/datasource/airports/build.gradle.kts
Normal file
15
model/datasource/airports/build.gradle.kts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.flights.android.library)
|
||||||
|
alias(libs.plugins.flights.android.library.hilt)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "dev.adriankuta.flights.model.datasource.airports"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(projects.model.datasource.shared)
|
||||||
|
implementation(projects.model.data.api)
|
||||||
|
|
||||||
|
implementation(libs.timber)
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package dev.adriankuta.flights.model.datasource.airports
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.model.datasource.airports.entities.AirportInfoModel
|
||||||
|
import dev.adriankuta.flights.model.datasource.shared.Cache
|
||||||
|
import dev.adriankuta.model.data.api.entities.AirportResponse
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
interface AirportsDatasource {
|
||||||
|
|
||||||
|
val airports: StateFlow<Cache<List<AirportInfoModel>>>
|
||||||
|
|
||||||
|
suspend fun setAirportsInfo(airports: List<AirportResponse>, cacheKey: String)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package dev.adriankuta.flights.model.datasource.airports.entities
|
||||||
|
|
||||||
|
interface AirportInfoModel {
|
||||||
|
val code: String
|
||||||
|
val name: String
|
||||||
|
val seoName: String
|
||||||
|
val isBase: Boolean
|
||||||
|
val timeZone: String
|
||||||
|
|
||||||
|
// City properties
|
||||||
|
val cityCode: String
|
||||||
|
val cityName: String
|
||||||
|
|
||||||
|
// MacCity properties
|
||||||
|
val macCode: String
|
||||||
|
|
||||||
|
// Region properties
|
||||||
|
val regionCode: String
|
||||||
|
val regionName: String
|
||||||
|
|
||||||
|
// Country properties
|
||||||
|
val countryCode: String
|
||||||
|
val countryName: String
|
||||||
|
val countryCurrencyCode: String
|
||||||
|
|
||||||
|
// Coordinates properties
|
||||||
|
val latitude: Double
|
||||||
|
val longitude: Double
|
||||||
|
}
|
12
model/datasource/shared/build.gradle.kts
Normal file
12
model/datasource/shared/build.gradle.kts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.flights.android.library)
|
||||||
|
alias(libs.plugins.flights.android.library.hilt)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "dev.adriankuta.flights.model.datasource.shared"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.timber)
|
||||||
|
}
|
10
model/datasource/shared/config/detekt/detekt.yml
Normal file
10
model/datasource/shared/config/detekt/detekt.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Deviations from defaults
|
||||||
|
formatting:
|
||||||
|
TrailingCommaOnCallSite:
|
||||||
|
active: true
|
||||||
|
autoCorrect: true
|
||||||
|
useTrailingCommaOnCallSite: true
|
||||||
|
TrailingCommaOnDeclarationSite:
|
||||||
|
active: true
|
||||||
|
autoCorrect: true
|
||||||
|
useTrailingCommaOnDeclarationSite: true
|
@ -0,0 +1,6 @@
|
|||||||
|
package dev.adriankuta.flights.model.datasource.shared;
|
||||||
|
|
||||||
|
public interface Cache<T> {
|
||||||
|
public val cacheKey: String?
|
||||||
|
public val data: T?
|
||||||
|
}
|
@ -8,7 +8,12 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.timber)
|
implementation(projects.domain.search)
|
||||||
|
implementation(projects.model.data.api)
|
||||||
|
implementation(projects.model.datasource.airports)
|
||||||
|
|
||||||
testImplementation("io.mockk:mockk:1.13.8")
|
implementation(libs.timber)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
|
|
||||||
|
testImplementation(libs.mockk.android)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
package dev.adriankuta.flights.model.repository.di
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import dev.adriankuta.flights.domain.search.ObserveAirportsUseCase
|
||||||
|
import dev.adriankuta.flights.model.repository.usecases.ObserveAirportsUseCaseImpl
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
@Suppress("UnnecessaryAbstractClass")
|
||||||
|
internal abstract class ObserveAirportsUseCaseModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bind(
|
||||||
|
observeAirportsUseCaseImpl: ObserveAirportsUseCaseImpl
|
||||||
|
): ObserveAirportsUseCase
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package dev.adriankuta.flights.model.repository.mappers
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||||
|
import dev.adriankuta.flights.domain.types.City
|
||||||
|
import dev.adriankuta.flights.domain.types.Coordinates
|
||||||
|
import dev.adriankuta.flights.domain.types.Country
|
||||||
|
import dev.adriankuta.flights.domain.types.MacCity
|
||||||
|
import dev.adriankuta.flights.domain.types.Region
|
||||||
|
import dev.adriankuta.flights.model.datasource.airports.entities.AirportInfoModel
|
||||||
|
|
||||||
|
internal fun AirportInfoModel.toDomain(): AirportInfo {
|
||||||
|
return AirportInfo(
|
||||||
|
code = code,
|
||||||
|
name = name,
|
||||||
|
seoName = seoName,
|
||||||
|
isBase = isBase,
|
||||||
|
timeZone = timeZone,
|
||||||
|
city = City(
|
||||||
|
code = cityCode,
|
||||||
|
name = cityName
|
||||||
|
),
|
||||||
|
macCity = MacCity(
|
||||||
|
macCode = macCode
|
||||||
|
),
|
||||||
|
region = Region(
|
||||||
|
code = regionCode,
|
||||||
|
name = regionName
|
||||||
|
),
|
||||||
|
country = Country(
|
||||||
|
code = countryCode,
|
||||||
|
name = countryName,
|
||||||
|
currencyCode = countryCurrencyCode
|
||||||
|
),
|
||||||
|
coordinates = Coordinates(
|
||||||
|
latitude = latitude,
|
||||||
|
longitude = longitude
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package dev.adriankuta.flights.model.repository.usecases
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.domain.search.ObserveAirportsUseCase
|
||||||
|
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||||
|
import dev.adriankuta.flights.model.datasource.airports.AirportsDatasource
|
||||||
|
import dev.adriankuta.flights.model.datasource.airports.entities.AirportInfoModel
|
||||||
|
import dev.adriankuta.flights.model.repository.mappers.toDomain
|
||||||
|
import dev.adriankuta.flights.model.repository.utilities.loadData
|
||||||
|
import dev.adriankuta.model.data.api.AirportService
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class ObserveAirportsUseCaseImpl @Inject constructor(
|
||||||
|
private val airportService: AirportService,
|
||||||
|
private val airportsDatasource: AirportsDatasource,
|
||||||
|
) : ObserveAirportsUseCase {
|
||||||
|
override fun invoke(): Flow<List<AirportInfo>?> = loadData(
|
||||||
|
onCacheInvalidated = {
|
||||||
|
val response = airportService.getAirports("PL")
|
||||||
|
airportsDatasource.setAirportsInfo(response, "PL")
|
||||||
|
},
|
||||||
|
observeCache = {
|
||||||
|
airportsDatasource.airports
|
||||||
|
},
|
||||||
|
mapToDomain = {
|
||||||
|
it.orEmpty().map(AirportInfoModel::toDomain)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package dev.adriankuta.flights.model.repository.utilities
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.model.datasource.shared.Cache
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
|
internal fun <T, R> loadData(
|
||||||
|
onCacheInvalidated: suspend (cacheKey: String) -> Unit,
|
||||||
|
observeCache: () -> Flow<Cache<T>>,
|
||||||
|
mapToDomain: (T?) -> R?,
|
||||||
|
): Flow<R?> {
|
||||||
|
return observeCache().distinctUntilChanged().map {
|
||||||
|
if (it.cacheKey == null) {
|
||||||
|
val refreshTime = Clock.System.now()
|
||||||
|
onCacheInvalidated(it.cacheKey ?: refreshTime.toEpochMilliseconds().toString())
|
||||||
|
}
|
||||||
|
mapToDomain(it.data)
|
||||||
|
}
|
||||||
|
}
|
@ -24,11 +24,14 @@ rootProject.name = "Flights"
|
|||||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||||
include(":app")
|
include(":app")
|
||||||
include(":core:util")
|
include(":core:util")
|
||||||
|
include(":domain:search")
|
||||||
include(":domain:types")
|
include(":domain:types")
|
||||||
include(":model:data:api")
|
include(":model:data:api")
|
||||||
include(":model:data:room")
|
include(":model:data:room")
|
||||||
|
include(":model:data:shared")
|
||||||
include(":model:data:simple")
|
include(":model:data:simple")
|
||||||
include(":model:datasource:characters")
|
include(":model:datasource:airports")
|
||||||
|
include(":model:datasource:shared")
|
||||||
include(":model:repository")
|
include(":model:repository")
|
||||||
include(":ui:designsystem")
|
include(":ui:designsystem")
|
||||||
include(":ui:home")
|
include(":ui:home")
|
||||||
|
@ -10,6 +10,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.ui.designsystem)
|
implementation(projects.ui.designsystem)
|
||||||
|
implementation(projects.domain.search)
|
||||||
|
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
package dev.adriankuta.flights.ui.home
|
package dev.adriankuta.flights.ui.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
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.FlightsTheme
|
||||||
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
||||||
|
|
||||||
@ -9,12 +16,31 @@ import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
|||||||
internal fun HomeScreen(
|
internal fun HomeScreen(
|
||||||
viewModel: HomeScreenViewModel = hiltViewModel(),
|
viewModel: HomeScreenViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
|
val homeUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
HomeScreen(
|
||||||
|
airports = homeUiState.airports
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HomeScreen(
|
||||||
|
airports: List<AirportInfo>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn {
|
||||||
|
items(airports) { airport ->
|
||||||
|
Text(airport.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewDevices
|
@PreviewDevices
|
||||||
@Composable
|
@Composable
|
||||||
private fun HomeScreenPreview() {
|
private fun HomeScreenPreview() {
|
||||||
FlightsTheme {
|
FlightsTheme {
|
||||||
HomeScreen()
|
HomeScreen(
|
||||||
|
airports = listOf()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,47 @@
|
|||||||
package dev.adriankuta.flights.ui.home
|
package dev.adriankuta.flights.ui.home
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
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 javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeScreenViewModel @Inject constructor() : ViewModel()
|
class HomeScreenViewModel @Inject constructor(
|
||||||
|
private val observeAirportsUseCase: ObserveAirportsUseCase
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
internal val uiState = homeUiState(
|
||||||
|
useCase = observeAirportsUseCase
|
||||||
|
)
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000),
|
||||||
|
initialValue = HomeUiState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun homeUiState(
|
||||||
|
useCase: ObserveAirportsUseCase
|
||||||
|
): Flow<HomeUiState> {
|
||||||
|
|
||||||
|
return flow {
|
||||||
|
delay(15.seconds)
|
||||||
|
emit("")
|
||||||
|
}.flatMapLatest { useCase() }.map {
|
||||||
|
HomeUiState(it.orEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class HomeUiState(
|
||||||
|
val airports: List<AirportInfo> = emptyList()
|
||||||
|
)
|
||||||
|
Reference in New Issue
Block a user