feat: Implement stations screen with grouped airports

This commit introduces a new "Stations" screen that displays airports grouped by country.

Key changes:
- Added a new Gradle module: `ui:stations`.
- Created `StationsScreen.kt` to display a list of airports grouped by country using `LazyColumn`. Airports within each country are displayed as `AirportInfoItem` and countries as `CountryItem`.
- Implemented `StationsScreenViewModel.kt` to fetch and manage the state of airports grouped by country. It uses `ObserveAirportsGroupedByCountry` use case.
- Defined `ObserveAirportsGroupedByCountry.kt` use case in `domain:stations` module to provide a flow of airports grouped by country.
- Implemented `ObserveAirportsGroupedByCountryImpl.kt` in the repository layer, which fetches data using `AirportService`, stores it in `AirportsDatasource`, and maps it to the domain model.
- Added Hilt module `ObserveAirportsGroupedByCountryModule.kt` to provide the use case implementation.
- Added `stationsScreen()` and `navigateToStations()` to `FlightsNavGraph.kt` and `TopLevelDestination.kt` for navigation.
- Updated `settings.gradle.kts` and `app/build.gradle.kts` to include the new `ui:stations` and `domain:stations` modules.
- Updated `CacheObservers.kt` to make `mapToDomain` a suspend function.
- Added string resources for the stations screen title.
This commit is contained in:
2025-06-15 23:15:25 +02:00
parent 13348bc52f
commit 3e9768919d
18 changed files with 368 additions and 5 deletions

View File

@ -8,7 +8,9 @@ android {
}
dependencies {
implementation(projects.core.util)
implementation(projects.domain.search)
implementation(projects.domain.stations)
implementation(projects.model.data.api)
implementation(projects.model.datasource.airports)

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.stations.ObserveAirportsGroupedByCountry
import dev.adriankuta.flights.model.repository.usecases.ObserveAirportsGroupedByCountryImpl
@Module
@InstallIn(SingletonComponent::class)
@Suppress("UnnecessaryAbstractClass")
internal abstract class ObserveAirportsGroupedByCountryModule {
@Binds
abstract fun bind(
observeAirportsGroupedByCountryImpl: ObserveAirportsGroupedByCountryImpl,
): ObserveAirportsGroupedByCountry
}

View File

@ -0,0 +1,38 @@
package dev.adriankuta.flights.model.repository.usecases
import dev.adriankuta.flights.core.util.DefaultDispatcher
import dev.adriankuta.flights.domain.stations.ObserveAirportsGroupedByCountry
import dev.adriankuta.flights.domain.types.AirportInfo
import dev.adriankuta.flights.domain.types.Country
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.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import javax.inject.Inject
internal class ObserveAirportsGroupedByCountryImpl @Inject constructor(
private val airportService: AirportService,
private val airportsDatasource: AirportsDatasource,
@DefaultDispatcher private val dispatcher: CoroutineDispatcher,
) : ObserveAirportsGroupedByCountry {
override fun invoke(): Flow<Map<Country, List<AirportInfo>>?> = loadData(
onCacheInvalidated = { cacheKey ->
val response = airportService.getAirports("pl")
airportsDatasource.setAirportsInfo(response, cacheKey)
},
observeCache = {
airportsDatasource.airports
},
mapToDomain = { model ->
withContext(dispatcher) {
model.orEmpty().map(AirportInfoModel::toDomain).groupBy { airportInfo ->
airportInfo.country
}.toSortedMap(compareBy { it.name })
}
},
)
}

View File

@ -9,7 +9,7 @@ import kotlinx.datetime.Clock
internal fun <T, R> loadData(
onCacheInvalidated: suspend (cacheKey: String) -> Unit,
observeCache: () -> Flow<Cache<T>>,
mapToDomain: (T?) -> R?,
mapToDomain: suspend (T?) -> R?,
): Flow<R?> {
return observeCache().distinctUntilChanged().map {
if (it.cacheKey == null) {