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:
2025-06-14 13:34:41 +02:00
parent b23baa587c
commit 524a64a443
5 changed files with 167 additions and 108 deletions

View File

@ -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 = {},
) )
} }
} }

View File

@ -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")
} }
} }

View File

@ -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,
)
}
}

View File

@ -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")
}
}
}

View File

@ -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,
) )
} }
} }