mirror of
				https://github.com/AdrianKuta/android-challange-adrian-kuta.git
				synced 2025-10-31 05:43:40 +01:00 
			
		
		
		
	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.
			
			
This commit is contained in:
		| @@ -1,16 +1,10 @@ | |||||||
| package dev.adriankuta.flights.ui.home | package dev.adriankuta.flights.ui.home | ||||||
|  |  | ||||||
| import androidx.compose.foundation.layout.Arrangement | import androidx.compose.foundation.layout.Box | ||||||
| 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.padding | 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.Text | ||||||
| import androidx.compose.material3.VerticalDivider |  | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
| import androidx.compose.runtime.getValue | import androidx.compose.runtime.getValue | ||||||
| import androidx.compose.ui.Modifier | 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.domain.types.Region | ||||||
| import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme | import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme | ||||||
| import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices | import dev.adriankuta.flights.ui.designsystem.theme.PreviewDevices | ||||||
| import dev.adriankuta.flights.ui.home.components.AirportDropdown | import dev.adriankuta.flights.ui.home.components.SearchForm | ||||||
| import dev.adriankuta.flights.ui.home.components.DatePicker |  | ||||||
| import dev.adriankuta.flights.ui.sharedui.Counter |  | ||||||
| import java.time.LocalDate | import java.time.LocalDate | ||||||
|  |  | ||||||
| @Composable | @Composable | ||||||
| @@ -44,6 +36,7 @@ internal fun HomeScreen( | |||||||
|         onAdultCountChange = viewModel::updateAdultCount, |         onAdultCountChange = viewModel::updateAdultCount, | ||||||
|         onTeenCountChange = viewModel::updateTeenCount, |         onTeenCountChange = viewModel::updateTeenCount, | ||||||
|         onChildCountChange = viewModel::updateChildCount, |         onChildCountChange = viewModel::updateChildCount, | ||||||
|  |         onSearch = viewModel::search, | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -56,10 +49,16 @@ private fun HomeScreen( | |||||||
|     onAdultCountChange: (Int) -> Unit, |     onAdultCountChange: (Int) -> Unit, | ||||||
|     onTeenCountChange: (Int) -> Unit, |     onTeenCountChange: (Int) -> Unit, | ||||||
|     onChildCountChange: (Int) -> Unit, |     onChildCountChange: (Int) -> Unit, | ||||||
|  |     onSearch: () -> Unit, | ||||||
|     modifier: Modifier = Modifier, |     modifier: Modifier = Modifier, | ||||||
| ) { | ) { | ||||||
|     Column( |     val scrollState = rememberScrollState() | ||||||
|         modifier = modifier.padding(16.dp), |     Box( | ||||||
|  |         modifier = modifier | ||||||
|  |             .padding(16.dp) | ||||||
|  |             .verticalScroll( | ||||||
|  |                 state = scrollState, | ||||||
|  |             ), | ||||||
|     ) { |     ) { | ||||||
|         when (uiState) { |         when (uiState) { | ||||||
|             is HomeUiState.Error -> Text("Error") |             is HomeUiState.Error -> Text("Error") | ||||||
| @@ -72,90 +71,12 @@ private fun HomeScreen( | |||||||
|                 onAdultCountChange = onAdultCountChange, |                 onAdultCountChange = onAdultCountChange, | ||||||
|                 onTeenCountChange = onTeenCountChange, |                 onTeenCountChange = onTeenCountChange, | ||||||
|                 onChildCountChange = onChildCountChange, |                 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 | @PreviewDevices | ||||||
| @Composable | @Composable | ||||||
| private fun HomeScreenLoadingPreview() { | private fun HomeScreenLoadingPreview() { | ||||||
| @@ -168,6 +89,7 @@ private fun HomeScreenLoadingPreview() { | |||||||
|             onAdultCountChange = {}, |             onAdultCountChange = {}, | ||||||
|             onTeenCountChange = {}, |             onTeenCountChange = {}, | ||||||
|             onChildCountChange = {}, |             onChildCountChange = {}, | ||||||
|  |             onSearch = {}, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -222,6 +144,7 @@ private fun HomeScreenSuccessPreview() { | |||||||
|             onAdultCountChange = {}, |             onAdultCountChange = {}, | ||||||
|             onTeenCountChange = {}, |             onTeenCountChange = {}, | ||||||
|             onChildCountChange = {}, |             onChildCountChange = {}, | ||||||
|  |             onSearch = {}, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,12 +13,13 @@ 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 timber.log.Timber | ||||||
| import java.time.LocalDate | import java.time.LocalDate | ||||||
| import javax.inject.Inject | import javax.inject.Inject | ||||||
|  |  | ||||||
| @HiltViewModel | @HiltViewModel | ||||||
| class HomeScreenViewModel @Inject constructor( | class HomeScreenViewModel @Inject constructor( | ||||||
|     private val observeAirportsUseCase: ObserveAirportsUseCase, |     observeAirportsUseCase: ObserveAirportsUseCase, | ||||||
| ) : ViewModel() { | ) : ViewModel() { | ||||||
|  |  | ||||||
|     private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null) |     private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null) | ||||||
| @@ -71,19 +72,21 @@ class HomeScreenViewModel @Inject constructor( | |||||||
|         selectedDate.value = date |         selectedDate.value = date | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun updateAdultCount(diff: Int) { |     fun updateAdultCount(count: Int) { | ||||||
|         val newCount = adultCount.value + diff |         adultCount.value = count | ||||||
|         adultCount.value = newCount |         childCount.value = childCount.value.coerceAtMost(count) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun updateTeenCount(diff: Int) { |     fun updateTeenCount(count: Int) { | ||||||
|         val newCount = teenCount.value + diff |         teenCount.value = count | ||||||
|         teenCount.value = newCount |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun updateChildCount(diff: Int) { |     fun updateChildCount(count: Int) { | ||||||
|         val newCount = childCount.value + diff |         childCount.value = count | ||||||
|         childCount.value = newCount |     } | ||||||
|  |  | ||||||
|  |     fun search() { | ||||||
|  |         Timber.d("Search clicked") | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -34,10 +34,11 @@ import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme | |||||||
| @Composable | @Composable | ||||||
| fun Counter( | fun Counter( | ||||||
|     value: Int, |     value: Int, | ||||||
|     onCountChange: (diff: Int) -> Unit, |     onValueChange: (newValue: Int) -> Unit, | ||||||
|     modifier: Modifier = Modifier, |     modifier: Modifier = Modifier, | ||||||
|     label: String? = null, |     label: String? = null, | ||||||
|     minVal: Int = 0, |     minVal: Int = 0, | ||||||
|  |     maxVal: Int = Int.MAX_VALUE, | ||||||
| ) { | ) { | ||||||
|     val view = LocalView.current |     val view = LocalView.current | ||||||
|  |  | ||||||
| @@ -84,7 +85,7 @@ fun Counter( | |||||||
|                 modifier = Modifier.weight(1f), |                 modifier = Modifier.weight(1f), | ||||||
|                 onClick = { |                 onClick = { | ||||||
|                     view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) |                     view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) | ||||||
|                     onCountChange(-1) |                     onValueChange(value - 1) | ||||||
|                 }, |                 }, | ||||||
|                 shape = MaterialTheme.shapes.medium, |                 shape = MaterialTheme.shapes.medium, | ||||||
|                 enabled = value > minVal, |                 enabled = value > minVal, | ||||||
| @@ -99,10 +100,10 @@ fun Counter( | |||||||
|                 modifier = Modifier.weight(1f), |                 modifier = Modifier.weight(1f), | ||||||
|                 onClick = { |                 onClick = { | ||||||
|                     view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) |                     view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) | ||||||
|                     onCountChange(1) |                     onValueChange(value + 1) | ||||||
|                 }, |                 }, | ||||||
|                 shape = MaterialTheme.shapes.medium, |                 shape = MaterialTheme.shapes.medium, | ||||||
|                 enabled = value < 20, |                 enabled = value < maxVal, | ||||||
|             ) { |             ) { | ||||||
|                 Text( |                 Text( | ||||||
|                     text = "+", |                     text = "+", | ||||||
| @@ -124,7 +125,8 @@ private fun CounterPreview() { | |||||||
|         Counter( |         Counter( | ||||||
|             value = tapCounter, |             value = tapCounter, | ||||||
|             label = "Taps", |             label = "Taps", | ||||||
|             onCountChange = { tapCounter += it }, |             onValueChange = { tapCounter += it }, | ||||||
|  |             maxVal = 20, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user