feat: Add passenger counter to home screen

This commit introduces a passenger counter component to the home screen, allowing users to specify the number of adults, teens, and children for their flight search.

Key changes:
- Added `ui:sharedui` module dependency to `ui:home`.
- Updated `PreviewDevices` annotation to include `showBackground = true`.
- Modified `HomeScreen`:
    - Added parameters to `HomeContent` and `SearchForm` for passenger count change callbacks.
    - Integrated the `Counter` composable from `ui:sharedui` for adults, teens, and children.
    - Added `VerticalDivider` between counters.
    - Updated previews to reflect passenger counter integration.
- Updated `HomeScreenViewModel`:
    - Introduced `MutableStateFlow` for `adultCount`, `teenCount`, and `childCount`.
    - Created `passengersState` flow by combining individual passenger count flows.
    - Updated `homeUiState` to include `passengersState`.
    - Added public functions `updateAdultCount`, `updateTeenCount`, and `updateChildCount` to modify passenger counts with validation.
    - Added `PassengersState` data class to hold passenger counts.
- Updated `Counter` composable in `ui:sharedui`:
    - Added an optional `minVal` parameter (defaulting to 0) to define the minimum allowed value for the counter.
    - Ensured the decrement button is disabled when the counter value reaches `minVal`.
    - Centered text in increment and decrement buttons.
This commit is contained in:
2025-06-14 09:40:59 +02:00
parent 6f6b886418
commit b23baa587c
5 changed files with 177 additions and 40 deletions

View File

@ -2,8 +2,8 @@ package dev.adriankuta.flights.ui.designsystem.theme
import androidx.compose.ui.tooling.preview.Preview
@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480")
@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480")
@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480")
@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480")
@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480", showBackground = true)
@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480", showBackground = true)
@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480", showBackground = true)
@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480", showBackground = true)
annotation class PreviewDevices

View File

@ -11,8 +11,9 @@ android {
dependencies {
implementation(projects.core.util)
implementation(projects.ui.designsystem)
implementation(projects.ui.sharedui)
implementation(projects.domain.search)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.timber)
}
}

View File

@ -1,11 +1,16 @@
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.padding
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
@ -22,6 +27,7 @@ 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 java.time.LocalDate
@Composable
@ -35,6 +41,9 @@ internal fun HomeScreen(
onOriginAirportSelect = viewModel::selectOriginAirport,
onDestinationAirportSelect = viewModel::selectDestinationAirport,
onDateSelect = viewModel::selectDate,
onAdultCountChange = viewModel::updateAdultCount,
onTeenCountChange = viewModel::updateTeenCount,
onChildCountChange = viewModel::updateChildCount,
)
}
@ -44,6 +53,9 @@ private fun HomeScreen(
onOriginAirportSelect: (AirportInfo) -> Unit,
onDestinationAirportSelect: (AirportInfo) -> Unit,
onDateSelect: (LocalDate) -> Unit,
onAdultCountChange: (Int) -> Unit,
onTeenCountChange: (Int) -> Unit,
onChildCountChange: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -52,40 +64,98 @@ private fun HomeScreen(
when (uiState) {
is HomeUiState.Error -> Text("Error")
HomeUiState.Loading -> Text("Loading")
is HomeUiState.Success -> {
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,
)
}
is HomeUiState.Success -> SearchForm(
uiState = uiState,
onOriginAirportSelect = onOriginAirportSelect,
onDestinationAirportSelect = onDestinationAirportSelect,
onDateSelect = onDateSelect,
onAdultCountChange = onAdultCountChange,
onTeenCountChange = onTeenCountChange,
onChildCountChange = onChildCountChange,
)
}
}
}
@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() {
@ -95,6 +165,9 @@ private fun HomeScreenLoadingPreview() {
onOriginAirportSelect = {},
onDestinationAirportSelect = {},
onDateSelect = {},
onAdultCountChange = {},
onTeenCountChange = {},
onChildCountChange = {},
)
}
}
@ -137,10 +210,18 @@ private fun HomeScreenSuccessPreview() {
selectedOriginAirport = mockAirports.first(),
selectedDestinationAirport = mockAirports.last(),
selectedDate = LocalDate.now(),
passengers = PassengersState(
adultCount = 2,
teenCount = 1,
childCount = 1,
),
),
onOriginAirportSelect = {},
onDestinationAirportSelect = {},
onDateSelect = {},
onAdultCountChange = {},
onTeenCountChange = {},
onChildCountChange = {},
)
}
}

View File

@ -10,6 +10,7 @@ import dev.adriankuta.flights.domain.types.AirportInfo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import java.time.LocalDate
@ -23,12 +24,20 @@ class HomeScreenViewModel @Inject constructor(
private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
private val selectedDestinationAirport = MutableStateFlow<AirportInfo?>(null)
private val selectedDate = MutableStateFlow(LocalDate.now())
private val adultCount = MutableStateFlow(1)
private val teenCount = MutableStateFlow(0)
private val childCount = MutableStateFlow(0)
internal val uiState = homeUiState(
useCase = observeAirportsUseCase,
selectedOriginAirport = selectedOriginAirport,
selectedDestinationAirport = selectedDestinationAirport,
selectedDate = selectedDate,
passengersState = passengersState(
adultCount = adultCount,
teenCount = teenCount,
childCount = childCount,
),
)
.stateIn(
scope = viewModelScope,
@ -61,20 +70,37 @@ class HomeScreenViewModel @Inject constructor(
fun selectDate(date: LocalDate) {
selectedDate.value = date
}
fun updateAdultCount(diff: Int) {
val newCount = adultCount.value + diff
adultCount.value = newCount
}
fun updateTeenCount(diff: Int) {
val newCount = teenCount.value + diff
teenCount.value = newCount
}
fun updateChildCount(diff: Int) {
val newCount = childCount.value + diff
childCount.value = newCount
}
}
private fun homeUiState(
useCase: ObserveAirportsUseCase,
selectedOriginAirport: MutableStateFlow<AirportInfo?>,
selectedDestinationAirport: MutableStateFlow<AirportInfo?>,
selectedDate: MutableStateFlow<LocalDate>,
selectedOriginAirport: StateFlow<AirportInfo?>,
selectedDestinationAirport: StateFlow<AirportInfo?>,
selectedDate: StateFlow<LocalDate>,
passengersState: Flow<PassengersState>,
): Flow<HomeUiState> {
return combine(
useCase().asResult(),
selectedOriginAirport,
selectedDestinationAirport,
selectedDate,
) { result, origin, destination, date ->
passengersState,
) { result, origin, destination, date, passengers ->
when (result) {
is Result.Error -> HomeUiState.Error(result.exception)
is Result.Loading -> HomeUiState.Loading
@ -86,12 +112,31 @@ private fun homeUiState(
selectedOriginAirport = origin,
selectedDestinationAirport = destination,
selectedDate = date,
passengers = passengers,
)
}
}
}
}
private fun passengersState(
adultCount: StateFlow<Int>,
teenCount: StateFlow<Int>,
childCount: StateFlow<Int>,
): Flow<PassengersState> {
return combine(
adultCount,
teenCount,
childCount,
) { adults, teens, children ->
PassengersState(
adultCount = adults,
teenCount = teens,
childCount = children,
)
}
}
internal sealed interface HomeUiState {
data object Loading : HomeUiState
data class Success(
@ -100,7 +145,14 @@ internal sealed interface HomeUiState {
val selectedOriginAirport: AirportInfo? = null,
val selectedDestinationAirport: AirportInfo? = null,
val selectedDate: LocalDate = LocalDate.now(),
val passengers: PassengersState,
) : HomeUiState
data class Error(val exception: Throwable) : HomeUiState
}
internal data class PassengersState(
val adultCount: Int,
val teenCount: Int,
val childCount: Int,
)

View File

@ -37,6 +37,7 @@ fun Counter(
onCountChange: (diff: Int) -> Unit,
modifier: Modifier = Modifier,
label: String? = null,
minVal: Int = 0,
) {
val view = LocalView.current
@ -86,11 +87,12 @@ fun Counter(
onCountChange(-1)
},
shape = MaterialTheme.shapes.medium,
enabled = value > 0,
enabled = value > minVal,
) {
Text(
text = "-",
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
)
}
Button(
@ -105,6 +107,7 @@ fun Counter(
Text(
text = "+",
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
)
}
}