feat: Implement flight search functionality

This commit introduces the ability to search for flights based on user-defined criteria.

Key changes:
- Added `GetFlightsSearchContentUseCase` interface in the domain layer and its implementation `GetFlightsSearchContentUseCaseImpl` in the repository layer to handle flight search logic.
- Implemented `FlightDomainMapper` to map `FlightResponse` from the API to the domain `Flight` model.
- Updated `NetworkModule`:
    - Introduced `ServicesAPI` and `NativeAppsAPI` qualifiers to differentiate between Retrofit instances for different Ryanair APIs.
    - Configured a new Retrofit instance for the `nativeapps.ryanair.com` base URL.
    - Modified `FlightService` to use the `nativeapps.ryanair.com` base URL.
- Updated `FlightService` endpoint to `api/v4/en-gb/Availability`.
- Integrated `GetFlightsSearchContentUseCase` into `HomeScreenViewModel` to trigger flight searches.
- Created `SearchOptions` data class in the domain layer to encapsulate search parameters.
- Added `GetFlightsSearchContentUseCaseModule` to provide the use case implementation via Hilt.
This commit is contained in:
2025-06-14 15:42:07 +02:00
parent 5d9a56d71f
commit ffcfc1f45b
8 changed files with 193 additions and 6 deletions

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.GetFlightsSearchContentUseCase
import dev.adriankuta.flights.model.repository.usecases.GetFlightsSearchContentUseCaseImpl
@Module
@InstallIn(SingletonComponent::class)
@Suppress("UnnecessaryAbstractClass")
internal abstract class GetFlightsSearchContentUseCaseModule {
@Binds
abstract fun bind(
getFlightsSearchContentUseCaseImpl: GetFlightsSearchContentUseCaseImpl,
): GetFlightsSearchContentUseCase
}

View File

@ -0,0 +1,75 @@
package dev.adriankuta.flights.model.repository.mappers
import dev.adriankuta.flights.domain.types.Flight
import dev.adriankuta.flights.domain.types.RegularFare
import dev.adriankuta.flights.domain.types.Segment
import dev.adriankuta.flights.domain.types.Trip
import dev.adriankuta.flights.domain.types.TripDate
import dev.adriankuta.flights.domain.types.TripFare
import dev.adriankuta.flights.domain.types.TripFlight
import dev.adriankuta.model.data.api.entities.FlightResponse
import dev.adriankuta.model.data.api.entities.RegularFare as ApiRegularFare
import dev.adriankuta.model.data.api.entities.Segment as ApiSegment
import dev.adriankuta.model.data.api.entities.Trip as ApiTrip
import dev.adriankuta.model.data.api.entities.TripDate as ApiTripDate
import dev.adriankuta.model.data.api.entities.TripFare as ApiTripFare
import dev.adriankuta.model.data.api.entities.TripFlight as ApiTripFlight
internal fun FlightResponse.toDomain(): Flight {
return Flight(
currency = currency ?: "",
currPrecision = currPrecision ?: 0,
trips = trips.orEmpty().map { it.toDomain() },
)
}
internal fun ApiTrip.toDomain(): Trip {
return Trip(
dates = dates.orEmpty().map { it.toDomain() },
origin = origin ?: "",
destination = destination ?: "",
)
}
internal fun ApiTripDate.toDomain(): TripDate {
return TripDate(
dateOut = dateOut ?: "",
flights = flights.orEmpty().map { it.toDomain() },
)
}
internal fun ApiTripFlight.toDomain(): TripFlight {
return TripFlight(
faresLeft = faresLeft ?: 0,
regularFare = regularFare?.toDomain() ?: RegularFare(emptyList()),
flightNumber = flightNumber ?: "",
dateTimes = dateTimes.orEmpty(),
duration = duration ?: "",
segments = segments.orEmpty().map { it.toDomain() },
operatedBy = operatedBy ?: "",
)
}
internal fun ApiRegularFare.toDomain(): RegularFare {
return RegularFare(
fares = fares.orEmpty().map { it.toDomain() },
)
}
internal fun ApiTripFare.toDomain(): TripFare {
return TripFare(
type = type ?: "",
amount = amount ?: 0.0,
count = count ?: 0,
)
}
internal fun ApiSegment.toDomain(): Segment {
return Segment(
origin = origin ?: "",
destination = destination ?: "",
flightNumber = flightNumber ?: "",
dateTimes = dateTimes.orEmpty(),
duration = duration ?: "",
)
}

View File

@ -0,0 +1,27 @@
package dev.adriankuta.flights.model.repository.usecases
import dev.adriankuta.flights.domain.search.GetFlightsSearchContentUseCase
import dev.adriankuta.flights.domain.search.entities.SearchOptions
import dev.adriankuta.flights.domain.types.Flight
import dev.adriankuta.flights.model.repository.mappers.toDomain
import dev.adriankuta.model.data.api.FlightService
import java.time.format.DateTimeFormatter
import javax.inject.Inject
internal class GetFlightsSearchContentUseCaseImpl @Inject constructor(
private val flightService: FlightService,
) : GetFlightsSearchContentUseCase {
override suspend fun invoke(searchOptions: SearchOptions): Flight {
val result = flightService.getFlights(
origin = searchOptions.origin.code,
destination = searchOptions.destination.code,
date = searchOptions.date.format(DateTimeFormatter.ISO_DATE),
adult = searchOptions.adults,
teen = searchOptions.teens,
child = searchOptions.children,
)
return result.toDomain()
}
}