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:
2025-06-13 21:45:55 +02:00
parent 0fb82d57dd
commit fcc84dc728
26 changed files with 437 additions and 5 deletions

View File

@ -8,7 +8,12 @@ android {
}
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)
}

View File

@ -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
}

View File

@ -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
)
)
}

View File

@ -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)
},
)
}

View File

@ -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)
}
}