mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-07-01 14:47:59 +02:00
feat: Implement airport connection fetching and filtering
This commit introduces the functionality to fetch available flight connections for a selected origin airport and filter the destination airport list accordingly. Key changes: - Added `GetConnectionsForAirportUseCase` interface in `domain/stations` to define the contract for fetching airport connections. - Implemented `GetConnectionsForAirportUseCaseImpl` in `model/repository` to fetch routes from the `RoutesService` and map them to domain `Airport` types. - Created `AirportDomainMapper.kt` in `model/repository/mappers` to map `RouteResponse` and its nested types to domain `Airport` and `MacCity` types. - Added a Hilt module `GetConnectionsForAirportUseCaseModule` to provide the implementation for `GetConnectionsForAirportUseCase`. - Renamed `OriginAirportsUiState` to `AirportsUiState` in `StationsScreen.kt` and `StationsScreenViewModel.kt` for better generality. - Updated `StationsScreenViewModel`: - Injected `GetConnectionsForAirportUseCase`. - Added `_availableDestinationsCodes` StateFlow to hold the codes of airports reachable from the selected origin. - Modified `airportsUiState` (previously `originAirportsUiState`) to take `filterDestinations` as a parameter and filter airports based on both search query and available destination codes. - Added `AirportInfo.toDepartureAirPort()` extension function. - Updated `StationsScreen.kt` to use the renamed `AirportsUiState`.
This commit is contained in:
@ -0,0 +1,8 @@
|
|||||||
|
package dev.adriankuta.flights.domain.stations
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.domain.types.Airport
|
||||||
|
|
||||||
|
fun interface GetConnectionsForAirportUseCase {
|
||||||
|
|
||||||
|
suspend operator fun invoke(airport: Airport): Set<Airport>
|
||||||
|
}
|
@ -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.GetConnectionsForAirportUseCase
|
||||||
|
import dev.adriankuta.flights.model.repository.usecases.GetConnectionsForAirportUseCaseImpl
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
@Suppress("UnnecessaryAbstractClass")
|
||||||
|
internal abstract class GetConnectionsForAirportUseCaseModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bind(
|
||||||
|
getConnectionsForAirportUseCaseImpl: GetConnectionsForAirportUseCaseImpl,
|
||||||
|
): GetConnectionsForAirportUseCase
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package dev.adriankuta.flights.model.repository.mappers
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.domain.types.Airport
|
||||||
|
import dev.adriankuta.flights.domain.types.MacCity
|
||||||
|
import dev.adriankuta.model.data.api.entities.RouteResponse
|
||||||
|
|
||||||
|
fun RouteResponse.Airport.MacCity.toDomain(): MacCity? {
|
||||||
|
val macCode = macCode ?: return null
|
||||||
|
return MacCity(macCode = macCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RouteResponse.toDomain(): List<Airport> {
|
||||||
|
val departureAirportDomain = departureAirport?.let {
|
||||||
|
Airport.Departure(
|
||||||
|
code = it.code ?: return@let null,
|
||||||
|
name = it.name ?: return@let null,
|
||||||
|
macCity = departureAirport?.macCity?.toDomain(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val arrivalAirportDomain = arrivalAirport?.let {
|
||||||
|
Airport.Arrival(
|
||||||
|
code = it.code ?: return@let null,
|
||||||
|
name = it.name ?: return@let null,
|
||||||
|
macCity = arrivalAirport?.macCity?.toDomain(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val connectingAirportDomain = connectingAirport?.let {
|
||||||
|
Airport.Connecting(
|
||||||
|
code = it.code ?: return@let null,
|
||||||
|
name = it.name ?: return@let null,
|
||||||
|
macCity = connectingAirport?.macCity?.toDomain(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return listOfNotNull(departureAirportDomain, arrivalAirportDomain, connectingAirportDomain)
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package dev.adriankuta.flights.model.repository.usecases
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.domain.stations.GetConnectionsForAirportUseCase
|
||||||
|
import dev.adriankuta.flights.domain.types.Airport
|
||||||
|
import dev.adriankuta.flights.model.repository.mappers.toDomain
|
||||||
|
import dev.adriankuta.model.data.api.RoutesService
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class GetConnectionsForAirportUseCaseImpl @Inject constructor(
|
||||||
|
private val routesService: RoutesService,
|
||||||
|
) : GetConnectionsForAirportUseCase {
|
||||||
|
override suspend fun invoke(airport: Airport): Set<Airport> {
|
||||||
|
val result = routesService.getRoutes(
|
||||||
|
languageCode = "pl",
|
||||||
|
departureAirportCode = airport.code,
|
||||||
|
)
|
||||||
|
return result.map { it.toDomain() }.flatten().toSet()
|
||||||
|
}
|
||||||
|
}
|
@ -57,7 +57,7 @@ internal fun StationsScreen(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun StationsScreen(
|
private fun StationsScreen(
|
||||||
originAirports: OriginAirportsUiState,
|
originAirports: AirportsUiState,
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
selectedOriginAirport: AirportInfo?,
|
selectedOriginAirport: AirportInfo?,
|
||||||
onSearchQueryChang: (String) -> Unit,
|
onSearchQueryChang: (String) -> Unit,
|
||||||
@ -91,9 +91,9 @@ private fun StationsScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
when (originAirports) {
|
when (originAirports) {
|
||||||
is OriginAirportsUiState.Error -> item { Text("Error") }
|
is AirportsUiState.Error -> item { Text("Error") }
|
||||||
is OriginAirportsUiState.Loading -> item { Text("Loading") }
|
is AirportsUiState.Loading -> item { Text("Loading") }
|
||||||
is OriginAirportsUiState.Success -> groupedAirports(
|
is AirportsUiState.Success -> groupedAirports(
|
||||||
data = originAirports.groupedAirports,
|
data = originAirports.groupedAirports,
|
||||||
onAirportSelected = onAirportSelect,
|
onAirportSelected = onAirportSelect,
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,9 @@ 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.Result
|
||||||
import dev.adriankuta.flights.core.util.asResult
|
import dev.adriankuta.flights.core.util.asResult
|
||||||
|
import dev.adriankuta.flights.domain.stations.GetConnectionsForAirportUseCase
|
||||||
import dev.adriankuta.flights.domain.stations.ObserveAirportsGroupedByCountry
|
import dev.adriankuta.flights.domain.stations.ObserveAirportsGroupedByCountry
|
||||||
|
import dev.adriankuta.flights.domain.types.Airport
|
||||||
import dev.adriankuta.flights.domain.types.AirportInfo
|
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||||
import dev.adriankuta.flights.domain.types.Country
|
import dev.adriankuta.flights.domain.types.Country
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -14,11 +16,13 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class StationsScreenViewModel @Inject constructor(
|
internal class StationsScreenViewModel @Inject constructor(
|
||||||
observeAirportsGroupedByCountry: ObserveAirportsGroupedByCountry,
|
observeAirportsGroupedByCountry: ObserveAirportsGroupedByCountry,
|
||||||
|
getConnectionsForAirportUseCase: GetConnectionsForAirportUseCase,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _searchQuery = MutableStateFlow("")
|
private val _searchQuery = MutableStateFlow("")
|
||||||
@ -26,6 +30,14 @@ internal class StationsScreenViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val _selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
|
private val _selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
|
||||||
val selectedOriginAirport: StateFlow<AirportInfo?> = _selectedOriginAirport
|
val selectedOriginAirport: StateFlow<AirportInfo?> = _selectedOriginAirport
|
||||||
|
private val _availableDestinationsCodes = selectedOriginAirport.transformLatest { airportInfo ->
|
||||||
|
val result = airportInfo?.let { nonNullAirportInfo ->
|
||||||
|
getConnectionsForAirportUseCase(nonNullAirportInfo.toDepartureAirPort())
|
||||||
|
.filter { it is Airport.Arrival }
|
||||||
|
.map { it.code }
|
||||||
|
}.orEmpty()
|
||||||
|
emit(result)
|
||||||
|
}
|
||||||
|
|
||||||
fun onSearchQueryChanged(query: String) {
|
fun onSearchQueryChanged(query: String) {
|
||||||
_searchQuery.value = query
|
_searchQuery.value = query
|
||||||
@ -39,55 +51,62 @@ internal class StationsScreenViewModel @Inject constructor(
|
|||||||
_selectedOriginAirport.value = null
|
_selectedOriginAirport.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
val originAirports = originAirportsUiState(
|
val originAirports = airportsUiState(
|
||||||
useCase = observeAirportsGroupedByCountry,
|
useCase = observeAirportsGroupedByCountry,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
|
filterDestinations = _availableDestinationsCodes,
|
||||||
).stateIn(
|
).stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.WhileSubscribed(5_000),
|
started = SharingStarted.WhileSubscribed(5_000),
|
||||||
initialValue = OriginAirportsUiState.Loading,
|
initialValue = AirportsUiState.Loading,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun originAirportsUiState(
|
private fun airportsUiState(
|
||||||
useCase: ObserveAirportsGroupedByCountry,
|
useCase: ObserveAirportsGroupedByCountry,
|
||||||
searchQuery: StateFlow<String>,
|
searchQuery: StateFlow<String>,
|
||||||
): Flow<OriginAirportsUiState> {
|
filterDestinations: Flow<List<String>>,
|
||||||
|
): Flow<AirportsUiState> {
|
||||||
return combine(
|
return combine(
|
||||||
useCase().asResult(),
|
useCase().asResult(),
|
||||||
searchQuery,
|
searchQuery,
|
||||||
) { result, query ->
|
filterDestinations,
|
||||||
|
) { result, query, destinationCodes ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Error -> OriginAirportsUiState.Error(result.exception)
|
is Result.Error -> AirportsUiState.Error(result.exception)
|
||||||
is Result.Loading -> OriginAirportsUiState.Loading
|
is Result.Loading -> AirportsUiState.Loading
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
val airports = result.data.orEmpty()
|
val airports = result.data.orEmpty()
|
||||||
if (query.isBlank()) {
|
if (query.isBlank() && destinationCodes.isEmpty()) {
|
||||||
OriginAirportsUiState.Success(airports)
|
AirportsUiState.Success(airports)
|
||||||
} else {
|
} else {
|
||||||
val filteredAirports = airports.mapValues { (_, airportList) ->
|
val filteredAirports = airports.mapValues { (_, airportList) ->
|
||||||
airportList.filter { airport ->
|
airportList.asSequence()
|
||||||
airport.name.contains(query, ignoreCase = true) ||
|
.filter { destinationCodes.isEmpty() || destinationCodes.contains(it.code) }
|
||||||
airport.code.contains(query, ignoreCase = true) ||
|
.filter { airport ->
|
||||||
airport.city.name.contains(query, ignoreCase = true) ||
|
airport.name.contains(query, ignoreCase = true) ||
|
||||||
airport.country.name.contains(query, ignoreCase = true)
|
airport.code.contains(query, ignoreCase = true) ||
|
||||||
}
|
airport.city.name.contains(query, ignoreCase = true) ||
|
||||||
|
airport.country.name.contains(query, ignoreCase = true)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
}.filterValues { it.isNotEmpty() }
|
}.filterValues { it.isNotEmpty() }
|
||||||
OriginAirportsUiState.Success(filteredAirports)
|
AirportsUiState.Success(filteredAirports)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed interface OriginAirportsUiState {
|
private fun AirportInfo.toDepartureAirPort(): Airport.Departure =
|
||||||
data object Loading : OriginAirportsUiState
|
Airport.Departure(
|
||||||
data class Success(val groupedAirports: Map<Country, List<AirportInfo>>) : OriginAirportsUiState
|
code = code,
|
||||||
data class Error(val exception: Throwable) : OriginAirportsUiState
|
name = name,
|
||||||
}
|
macCity = macCity,
|
||||||
|
)
|
||||||
|
|
||||||
internal sealed interface DestinationAirportsUiState {
|
internal sealed interface AirportsUiState {
|
||||||
data object Loading : DestinationAirportsUiState
|
data object Loading : AirportsUiState
|
||||||
data class Success(val airports: List<AirportInfo>) : DestinationAirportsUiState
|
data class Success(val groupedAirports: Map<Country, List<AirportInfo>>) : AirportsUiState
|
||||||
data class Error(val exception: Throwable) : DestinationAirportsUiState
|
data class Error(val exception: Throwable) : AirportsUiState
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user