mirror of
				https://github.com/AdrianKuta/android-challange-adrian-kuta.git
				synced 2025-10-31 05:43:40 +01: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