From 524a64a443fe3967ea2525f5c0e3c560f017c28a Mon Sep 17 00:00:00 2001 From: Adrian Kuta Date: Sat, 14 Jun 2025 13:34:41 +0200 Subject: [PATCH] Refactor: Introduce SearchForm and PassengersOptions composables This commit refactors the HomeScreen by extracting the search form logic into a new `SearchForm` composable and the passenger selection into a `PassengersOptions` composable. Key changes: - Created `PassengersOptions.kt` with a composable function to handle adult, teen, and child passenger counts. - Created `SearchForm.kt` with a composable function that encapsulates airport selection, date picking, passenger options, and the search button. - Updated `HomeScreen.kt`: - Replaced the previous inline form layout with the new `SearchForm` composable. - Made the screen content vertically scrollable using `Box` and `verticalScroll`. - Passed a new `onSearch` lambda to the `HomeScreenContent`. - Updated `HomeScreenViewModel.kt`: - Modified `updateAdultCount`, `updateTeenCount`, and `updateChildCount` to accept the new count directly instead of a diff. - Added a `search()` function (currently logs to Timber). - Ensured child count does not exceed adult count when adult count changes. - Updated `Counter.kt` in `ui/sharedui`: - Changed `onCountChange` parameter to `onValueChange` which now receives the new absolute value. - Added a `maxVal` parameter to limit the maximum value of the counter. --- .../adriankuta/flights/ui/home/HomeScreen.kt | 109 +++--------------- .../flights/ui/home/HomeScreenViewModel.kt | 23 ++-- .../ui/home/components/PassengersOptions.kt | 55 +++++++++ .../flights/ui/home/components/SearchForm.kt | 76 ++++++++++++ .../adriankuta/flights/ui/sharedui/Counter.kt | 12 +- 5 files changed, 167 insertions(+), 108 deletions(-) create mode 100644 ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/PassengersOptions.kt create mode 100644 ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/SearchForm.kt diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt index f50ce57..c975846 100644 --- a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreen.kt @@ -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 = {}, ) } } diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt index 2673a65..450d5af 100644 --- a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/HomeScreenViewModel.kt @@ -13,12 +13,13 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import timber.log.Timber import java.time.LocalDate import javax.inject.Inject @HiltViewModel class HomeScreenViewModel @Inject constructor( - private val observeAirportsUseCase: ObserveAirportsUseCase, + observeAirportsUseCase: ObserveAirportsUseCase, ) : ViewModel() { private val selectedOriginAirport = MutableStateFlow(null) @@ -71,19 +72,21 @@ 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() { + Timber.d("Search clicked") } } diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/PassengersOptions.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/PassengersOptions.kt new file mode 100644 index 0000000..b780895 --- /dev/null +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/PassengersOptions.kt @@ -0,0 +1,55 @@ +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.home.HomeUiState +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, + ) + } +} diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/SearchForm.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/SearchForm.kt new file mode 100644 index 0000000..48efd48 --- /dev/null +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/SearchForm.kt @@ -0,0 +1,76 @@ +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.ui.home.HomeUiState +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") + } + } +} diff --git a/ui/sharedui/src/main/kotlin/dev/adriankuta/flights/ui/sharedui/Counter.kt b/ui/sharedui/src/main/kotlin/dev/adriankuta/flights/ui/sharedui/Counter.kt index 04edae6..7d08ddf 100644 --- a/ui/sharedui/src/main/kotlin/dev/adriankuta/flights/ui/sharedui/Counter.kt +++ b/ui/sharedui/src/main/kotlin/dev/adriankuta/flights/ui/sharedui/Counter.kt @@ -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, ) } }