mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-07-01 20:17:58 +02:00
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:
@ -0,0 +1,8 @@
|
|||||||
|
package dev.adriankuta.flights.domain.search
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.domain.search.entities.SearchOptions
|
||||||
|
import dev.adriankuta.flights.domain.types.Flight
|
||||||
|
|
||||||
|
fun interface GetFlightsSearchContentUseCase {
|
||||||
|
suspend operator fun invoke(searchOptions: SearchOptions): Flight
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package dev.adriankuta.flights.domain.search.entities
|
||||||
|
|
||||||
|
import dev.adriankuta.flights.domain.types.Airport
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
data class SearchOptions(
|
||||||
|
val origin: Airport.Departure,
|
||||||
|
val destination: Airport.Arrival,
|
||||||
|
val date: LocalDate,
|
||||||
|
val adults: Int,
|
||||||
|
val teens: Int,
|
||||||
|
val children: Int,
|
||||||
|
)
|
@ -12,7 +12,7 @@ interface FlightService {
|
|||||||
"client: android",
|
"client: android",
|
||||||
"client-version: 3.300.0",
|
"client-version: 3.300.0",
|
||||||
)
|
)
|
||||||
@GET("v4/en-gb/Availability")
|
@GET("api/v4/en-gb/Availability")
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
suspend fun getFlights(
|
suspend fun getFlights(
|
||||||
@Query("dateout") date: String,
|
@Query("dateout") date: String,
|
||||||
|
@ -12,6 +12,7 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
|
import javax.inject.Qualifier
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ -37,7 +38,8 @@ class NetworkModule {
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideRetrofit(client: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder()
|
@ServicesAPI
|
||||||
|
fun provideServicesRetrofit(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")
|
.baseUrl("https://services-api.ryanair.com")
|
||||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
@ -46,16 +48,34 @@ class NetworkModule {
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideAirportService(retrofit: Retrofit): AirportService =
|
@NativeAppsAPI
|
||||||
|
fun provideNativeAppsRetrofit(client: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder()
|
||||||
|
// TODO("add URL provided in the task instructions")
|
||||||
|
.baseUrl("https://nativeapps.ryanair.com")
|
||||||
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
|
.client(client)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideAirportService(@ServicesAPI retrofit: Retrofit): AirportService =
|
||||||
retrofit.create(AirportService::class.java)
|
retrofit.create(AirportService::class.java)
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideRoutesService(retrofit: Retrofit): RoutesService =
|
fun provideRoutesService(@ServicesAPI retrofit: Retrofit): RoutesService =
|
||||||
retrofit.create(RoutesService::class.java)
|
retrofit.create(RoutesService::class.java)
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideFlightService(retrofit: Retrofit): FlightService =
|
fun provideFlightService(@NativeAppsAPI retrofit: Retrofit): FlightService =
|
||||||
retrofit.create(FlightService::class.java)
|
retrofit.create(FlightService::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class ServicesAPI
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class NativeAppsAPI
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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 ?: "",
|
||||||
|
)
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,10 @@ 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.search.GetFlightsSearchContentUseCase
|
||||||
import dev.adriankuta.flights.domain.search.ObserveAirportsUseCase
|
import dev.adriankuta.flights.domain.search.ObserveAirportsUseCase
|
||||||
|
import dev.adriankuta.flights.domain.search.entities.SearchOptions
|
||||||
|
import dev.adriankuta.flights.domain.types.Airport
|
||||||
import dev.adriankuta.flights.domain.types.AirportInfo
|
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ -13,6 +16,7 @@ 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.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -20,6 +24,7 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeScreenViewModel @Inject constructor(
|
class HomeScreenViewModel @Inject constructor(
|
||||||
observeAirportsUseCase: ObserveAirportsUseCase,
|
observeAirportsUseCase: ObserveAirportsUseCase,
|
||||||
|
private val getFlightsSearchContentUseCase: GetFlightsSearchContentUseCase,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
|
private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
|
||||||
@ -86,7 +91,27 @@ class HomeScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun search() {
|
fun search() {
|
||||||
Timber.d("Search clicked")
|
viewModelScope.launch {
|
||||||
|
val results = getFlightsSearchContentUseCase(
|
||||||
|
searchOptions = SearchOptions(
|
||||||
|
origin = Airport.Departure(
|
||||||
|
code = selectedOriginAirport.value?.code ?: return@launch,
|
||||||
|
name = selectedOriginAirport.value?.name ?: return@launch,
|
||||||
|
macCity = selectedOriginAirport.value?.macCity,
|
||||||
|
),
|
||||||
|
destination = Airport.Arrival(
|
||||||
|
code = selectedDestinationAirport.value?.code ?: return@launch,
|
||||||
|
name = selectedDestinationAirport.value?.name ?: return@launch,
|
||||||
|
macCity = selectedDestinationAirport.value?.macCity,
|
||||||
|
),
|
||||||
|
date = selectedDate.value,
|
||||||
|
adults = adultCount.value,
|
||||||
|
teens = teenCount.value,
|
||||||
|
children = childCount.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Timber.d("Result $results")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user