mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-07-01 21:07:59 +02:00
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:
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user