mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-09-14 21:04:22 +02:00
Compare commits
3 Commits
b23baa587c
...
ffcfc1f45b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ffcfc1f45b | ||
![]() |
5d9a56d71f | ||
![]() |
524a64a443 |
@@ -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-version: 3.300.0",
|
||||
)
|
||||
@GET("v4/en-gb/Availability")
|
||||
@GET("api/v4/en-gb/Availability")
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun getFlights(
|
||||
@Query("dateout") date: String,
|
||||
|
@@ -12,6 +12,7 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -37,7 +38,8 @@ class NetworkModule {
|
||||
|
||||
@Singleton
|
||||
@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")
|
||||
.baseUrl("https://services-api.ryanair.com")
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
@@ -46,16 +48,34 @@ class NetworkModule {
|
||||
|
||||
@Singleton
|
||||
@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)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideRoutesService(retrofit: Retrofit): RoutesService =
|
||||
fun provideRoutesService(@ServicesAPI retrofit: Retrofit): RoutesService =
|
||||
retrofit.create(RoutesService::class.java)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideFlightService(retrofit: Retrofit): FlightService =
|
||||
fun provideFlightService(@NativeAppsAPI retrofit: Retrofit): FlightService =
|
||||
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()
|
||||
}
|
||||
}
|
@@ -1,16 +1,10 @@
|
||||
package dev.adriankuta.flights.ui.home
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -25,9 +19,7 @@ import dev.adriankuta.flights.domain.types.MacCity
|
||||
import dev.adriankuta.flights.domain.types.Region
|
||||
import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
|
||||
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
||||
import dev.adriankuta.flights.ui.home.components.AirportDropdown
|
||||
import dev.adriankuta.flights.ui.home.components.DatePicker
|
||||
import dev.adriankuta.flights.ui.sharedui.Counter
|
||||
import dev.adriankuta.flights.ui.home.components.SearchForm
|
||||
import java.time.LocalDate
|
||||
|
||||
@Composable
|
||||
@@ -44,6 +36,7 @@ internal fun HomeScreen(
|
||||
onAdultCountChange = viewModel::updateAdultCount,
|
||||
onTeenCountChange = viewModel::updateTeenCount,
|
||||
onChildCountChange = viewModel::updateChildCount,
|
||||
onSearch = viewModel::search,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,10 +49,16 @@ private fun HomeScreen(
|
||||
onAdultCountChange: (Int) -> Unit,
|
||||
onTeenCountChange: (Int) -> Unit,
|
||||
onChildCountChange: (Int) -> Unit,
|
||||
onSearch: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(16.dp),
|
||||
val scrollState = rememberScrollState()
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(16.dp)
|
||||
.verticalScroll(
|
||||
state = scrollState,
|
||||
),
|
||||
) {
|
||||
when (uiState) {
|
||||
is HomeUiState.Error -> Text("Error")
|
||||
@@ -72,90 +71,12 @@ private fun HomeScreen(
|
||||
onAdultCountChange = onAdultCountChange,
|
||||
onTeenCountChange = onTeenCountChange,
|
||||
onChildCountChange = onChildCountChange,
|
||||
onSearch = onSearch,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.SearchForm(
|
||||
uiState: HomeUiState.Success,
|
||||
onOriginAirportSelect: (AirportInfo) -> Unit,
|
||||
onDestinationAirportSelect: (AirportInfo) -> Unit,
|
||||
onDateSelect: (LocalDate) -> Unit,
|
||||
onAdultCountChange: (Int) -> Unit,
|
||||
onTeenCountChange: (Int) -> Unit,
|
||||
onChildCountChange: (Int) -> Unit,
|
||||
) {
|
||||
AirportDropdown(
|
||||
label = "Origin Airport",
|
||||
airports = uiState.originAirports,
|
||||
selectedAirport = uiState.selectedOriginAirport,
|
||||
onAirportSelect = onOriginAirportSelect,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
AirportDropdown(
|
||||
label = "Destination Airport",
|
||||
airports = uiState.destinationAirports,
|
||||
selectedAirport = uiState.selectedDestinationAirport,
|
||||
onAirportSelect = onDestinationAirportSelect,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = uiState.selectedOriginAirport != null,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
DatePicker(
|
||||
label = "Departure Date",
|
||||
selectedDate = uiState.selectedDate,
|
||||
onDateSelect = onDateSelect,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = uiState.selectedDestinationAirport != null,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text("Passengers")
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(intrinsicSize = IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Counter(
|
||||
value = uiState.passengers.adultCount,
|
||||
onCountChange = onAdultCountChange,
|
||||
label = "Adults",
|
||||
modifier = Modifier.weight(1f),
|
||||
minVal = 1,
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
Counter(
|
||||
value = uiState.passengers.teenCount,
|
||||
onCountChange = onTeenCountChange,
|
||||
label = "Teens",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
Counter(
|
||||
value = uiState.passengers.childCount,
|
||||
onCountChange = onChildCountChange,
|
||||
label = "Children",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewDevices
|
||||
@Composable
|
||||
private fun HomeScreenLoadingPreview() {
|
||||
@@ -168,6 +89,7 @@ private fun HomeScreenLoadingPreview() {
|
||||
onAdultCountChange = {},
|
||||
onTeenCountChange = {},
|
||||
onChildCountChange = {},
|
||||
onSearch = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -222,6 +144,7 @@ private fun HomeScreenSuccessPreview() {
|
||||
onAdultCountChange = {},
|
||||
onTeenCountChange = {},
|
||||
onChildCountChange = {},
|
||||
onSearch = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,10 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.adriankuta.flights.core.util.Result
|
||||
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.entities.SearchOptions
|
||||
import dev.adriankuta.flights.domain.types.Airport
|
||||
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -13,12 +16,15 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeScreenViewModel @Inject constructor(
|
||||
private val observeAirportsUseCase: ObserveAirportsUseCase,
|
||||
observeAirportsUseCase: ObserveAirportsUseCase,
|
||||
private val getFlightsSearchContentUseCase: GetFlightsSearchContentUseCase,
|
||||
) : ViewModel() {
|
||||
|
||||
private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
|
||||
@@ -71,19 +77,41 @@ class HomeScreenViewModel @Inject constructor(
|
||||
selectedDate.value = date
|
||||
}
|
||||
|
||||
fun updateAdultCount(diff: Int) {
|
||||
val newCount = adultCount.value + diff
|
||||
adultCount.value = newCount
|
||||
fun updateAdultCount(count: Int) {
|
||||
adultCount.value = count
|
||||
childCount.value = childCount.value.coerceAtMost(count)
|
||||
}
|
||||
|
||||
fun updateTeenCount(diff: Int) {
|
||||
val newCount = teenCount.value + diff
|
||||
teenCount.value = newCount
|
||||
fun updateTeenCount(count: Int) {
|
||||
teenCount.value = count
|
||||
}
|
||||
|
||||
fun updateChildCount(diff: Int) {
|
||||
val newCount = childCount.value + diff
|
||||
childCount.value = newCount
|
||||
fun updateChildCount(count: Int) {
|
||||
childCount.value = count
|
||||
}
|
||||
|
||||
fun search() {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,13 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||
import dev.adriankuta.flights.domain.types.City
|
||||
import dev.adriankuta.flights.domain.types.Coordinates
|
||||
import dev.adriankuta.flights.domain.types.Country
|
||||
import dev.adriankuta.flights.domain.types.MacCity
|
||||
import dev.adriankuta.flights.domain.types.Region
|
||||
import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
|
||||
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -64,3 +71,75 @@ fun AirportDropdown(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@PreviewDevices
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
private fun AirportDropdownPreview() {
|
||||
val sampleAirports = listOf(
|
||||
AirportInfo(
|
||||
code = "WAW",
|
||||
name = "Warsaw Chopin",
|
||||
seoName = "warsaw",
|
||||
isBase = true,
|
||||
timeZone = "Europe/Warsaw",
|
||||
city = City(
|
||||
code = "WAW",
|
||||
name = "Warsaw",
|
||||
),
|
||||
macCity = MacCity(
|
||||
macCode = "WAW",
|
||||
),
|
||||
region = Region(
|
||||
code = "PL",
|
||||
name = "Poland",
|
||||
),
|
||||
country = Country(
|
||||
code = "PL",
|
||||
name = "Poland",
|
||||
currencyCode = "PLN",
|
||||
),
|
||||
coordinates = Coordinates(
|
||||
latitude = 52.1657,
|
||||
longitude = 20.9671,
|
||||
),
|
||||
),
|
||||
AirportInfo(
|
||||
code = "LHR",
|
||||
name = "London Heathrow",
|
||||
seoName = "london",
|
||||
isBase = false,
|
||||
timeZone = "Europe/London",
|
||||
city = City(
|
||||
code = "LON",
|
||||
name = "London",
|
||||
),
|
||||
macCity = MacCity(
|
||||
macCode = "LON",
|
||||
),
|
||||
region = Region(
|
||||
code = "UK",
|
||||
name = "United Kingdom",
|
||||
),
|
||||
country = Country(
|
||||
code = "UK",
|
||||
name = "United Kingdom",
|
||||
currencyCode = "GBP",
|
||||
),
|
||||
coordinates = Coordinates(
|
||||
latitude = 51.4700,
|
||||
longitude = -0.4543,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
FlightsTheme {
|
||||
AirportDropdown(
|
||||
label = "Departure Airport",
|
||||
airports = sampleAirports,
|
||||
selectedAirport = sampleAirports.first(),
|
||||
onAirportSelect = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
|
||||
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import java.time.Instant
|
||||
@@ -152,3 +154,19 @@ class FutureSelectableDates : SelectableDates {
|
||||
return year >= now.year
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@PreviewDevices
|
||||
@Composable
|
||||
private fun DatePickerPreview() {
|
||||
val today = LocalDate.now()
|
||||
val futureDate = today.plusDays(7) // A week from today
|
||||
|
||||
FlightsTheme {
|
||||
DatePicker(
|
||||
label = "Departure Date",
|
||||
selectedDate = futureDate,
|
||||
onDateSelect = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,86 @@
|
||||
package dev.adriankuta.flights.ui.home.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
|
||||
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
||||
import dev.adriankuta.flights.ui.home.HomeUiState
|
||||
import dev.adriankuta.flights.ui.home.PassengersState
|
||||
import dev.adriankuta.flights.ui.sharedui.Counter
|
||||
|
||||
@Composable
|
||||
internal fun PassengersOptions(
|
||||
uiState: HomeUiState.Success,
|
||||
onAdultCountChange: (Int) -> Unit,
|
||||
onTeenCountChange: (Int) -> Unit,
|
||||
onChildCountChange: (Int) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(intrinsicSize = IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Counter(
|
||||
value = uiState.passengers.adultCount,
|
||||
onValueChange = onAdultCountChange,
|
||||
label = "Adults",
|
||||
modifier = Modifier.weight(1f),
|
||||
minVal = 1,
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
Counter(
|
||||
value = uiState.passengers.teenCount,
|
||||
onValueChange = onTeenCountChange,
|
||||
label = "Teens",
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
Counter(
|
||||
value = uiState.passengers.childCount,
|
||||
onValueChange = onChildCountChange,
|
||||
label = "Children",
|
||||
modifier = Modifier.weight(1f),
|
||||
maxVal = uiState.passengers.adultCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewDevices
|
||||
@Composable
|
||||
private fun PassengersOptionsPreview() {
|
||||
val samplePassengersState = PassengersState(
|
||||
adultCount = 2,
|
||||
teenCount = 1,
|
||||
childCount = 1,
|
||||
)
|
||||
|
||||
val sampleUiState = HomeUiState.Success(
|
||||
originAirports = emptyList(),
|
||||
destinationAirports = emptyList(),
|
||||
selectedOriginAirport = null,
|
||||
selectedDestinationAirport = null,
|
||||
selectedDate = java.time.LocalDate.now(),
|
||||
passengers = samplePassengersState,
|
||||
)
|
||||
|
||||
FlightsTheme {
|
||||
PassengersOptions(
|
||||
uiState = sampleUiState,
|
||||
onAdultCountChange = {},
|
||||
onTeenCountChange = {},
|
||||
onChildCountChange = {},
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,174 @@
|
||||
package dev.adriankuta.flights.ui.home.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||
import dev.adriankuta.flights.domain.types.City
|
||||
import dev.adriankuta.flights.domain.types.Coordinates
|
||||
import dev.adriankuta.flights.domain.types.Country
|
||||
import dev.adriankuta.flights.domain.types.MacCity
|
||||
import dev.adriankuta.flights.domain.types.Region
|
||||
import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
|
||||
import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices
|
||||
import dev.adriankuta.flights.ui.home.HomeUiState
|
||||
import dev.adriankuta.flights.ui.home.PassengersState
|
||||
import java.time.LocalDate
|
||||
|
||||
@Composable
|
||||
internal fun SearchForm(
|
||||
uiState: HomeUiState.Success,
|
||||
onOriginAirportSelect: (AirportInfo) -> Unit,
|
||||
onDestinationAirportSelect: (AirportInfo) -> Unit,
|
||||
onDateSelect: (LocalDate) -> Unit,
|
||||
onAdultCountChange: (Int) -> Unit,
|
||||
onTeenCountChange: (Int) -> Unit,
|
||||
onChildCountChange: (Int) -> Unit,
|
||||
onSearch: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier) {
|
||||
AirportDropdown(
|
||||
label = "Origin Airport",
|
||||
airports = uiState.originAirports,
|
||||
selectedAirport = uiState.selectedOriginAirport,
|
||||
onAirportSelect = onOriginAirportSelect,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
AirportDropdown(
|
||||
label = "Destination Airport",
|
||||
airports = uiState.destinationAirports,
|
||||
selectedAirport = uiState.selectedDestinationAirport,
|
||||
onAirportSelect = onDestinationAirportSelect,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = uiState.selectedOriginAirport != null,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
DatePicker(
|
||||
label = "Departure Date",
|
||||
selectedDate = uiState.selectedDate,
|
||||
onDateSelect = onDateSelect,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = uiState.selectedDestinationAirport != null,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text("Passengers")
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
PassengersOptions(uiState, onAdultCountChange, onTeenCountChange, onChildCountChange)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = onSearch,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = uiState.selectedOriginAirport != null && uiState.selectedDestinationAirport != null,
|
||||
) {
|
||||
Text("Search Flights")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewDevices
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
private fun SearchFormPreview() {
|
||||
val sampleAirports = listOf(
|
||||
AirportInfo(
|
||||
code = "WAW",
|
||||
name = "Warsaw Chopin",
|
||||
seoName = "warsaw",
|
||||
isBase = true,
|
||||
timeZone = "Europe/Warsaw",
|
||||
city = City(
|
||||
code = "WAW",
|
||||
name = "Warsaw",
|
||||
),
|
||||
macCity = MacCity(
|
||||
macCode = "WAW",
|
||||
),
|
||||
region = Region(
|
||||
code = "PL",
|
||||
name = "Poland",
|
||||
),
|
||||
country = Country(
|
||||
code = "PL",
|
||||
name = "Poland",
|
||||
currencyCode = "PLN",
|
||||
),
|
||||
coordinates = Coordinates(
|
||||
latitude = 52.1657,
|
||||
longitude = 20.9671,
|
||||
),
|
||||
),
|
||||
AirportInfo(
|
||||
code = "LHR",
|
||||
name = "London Heathrow",
|
||||
seoName = "london",
|
||||
isBase = false,
|
||||
timeZone = "Europe/London",
|
||||
city = City(
|
||||
code = "LON",
|
||||
name = "London",
|
||||
),
|
||||
macCity = MacCity(
|
||||
macCode = "LON",
|
||||
),
|
||||
region = Region(
|
||||
code = "UK",
|
||||
name = "United Kingdom",
|
||||
),
|
||||
country = Country(
|
||||
code = "UK",
|
||||
name = "United Kingdom",
|
||||
currencyCode = "GBP",
|
||||
),
|
||||
coordinates = Coordinates(
|
||||
latitude = 51.4700,
|
||||
longitude = -0.4543,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val samplePassengersState = PassengersState(
|
||||
adultCount = 2,
|
||||
teenCount = 1,
|
||||
childCount = 1,
|
||||
)
|
||||
|
||||
val sampleUiState = HomeUiState.Success(
|
||||
originAirports = sampleAirports,
|
||||
destinationAirports = sampleAirports.drop(1),
|
||||
selectedOriginAirport = sampleAirports.first(),
|
||||
selectedDestinationAirport = sampleAirports.last(),
|
||||
selectedDate = LocalDate.now().plusDays(7),
|
||||
passengers = samplePassengersState,
|
||||
)
|
||||
|
||||
FlightsTheme {
|
||||
SearchForm(
|
||||
uiState = sampleUiState,
|
||||
onOriginAirportSelect = {},
|
||||
onDestinationAirportSelect = {},
|
||||
onDateSelect = {},
|
||||
onAdultCountChange = {},
|
||||
onTeenCountChange = {},
|
||||
onChildCountChange = {},
|
||||
onSearch = {},
|
||||
)
|
||||
}
|
||||
}
|
@@ -34,10 +34,11 @@ import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
|
||||
@Composable
|
||||
fun Counter(
|
||||
value: Int,
|
||||
onCountChange: (diff: Int) -> Unit,
|
||||
onValueChange: (newValue: Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
label: String? = null,
|
||||
minVal: Int = 0,
|
||||
maxVal: Int = Int.MAX_VALUE,
|
||||
) {
|
||||
val view = LocalView.current
|
||||
|
||||
@@ -84,7 +85,7 @@ fun Counter(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
onCountChange(-1)
|
||||
onValueChange(value - 1)
|
||||
},
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
enabled = value > minVal,
|
||||
@@ -99,10 +100,10 @@ fun Counter(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
onCountChange(1)
|
||||
onValueChange(value + 1)
|
||||
},
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
enabled = value < 20,
|
||||
enabled = value < maxVal,
|
||||
) {
|
||||
Text(
|
||||
text = "+",
|
||||
@@ -124,7 +125,8 @@ private fun CounterPreview() {
|
||||
Counter(
|
||||
value = tapCounter,
|
||||
label = "Taps",
|
||||
onCountChange = { tapCounter += it },
|
||||
onValueChange = { tapCounter += it },
|
||||
maxVal = 20,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user