mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-07-02 01:48:00 +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
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,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")
|
@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")
|
@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")
|
@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480", showBackground = true)
|
||||||
annotation class PreviewDevices
|
annotation class PreviewDevices
|
||||||
|
@ -11,6 +11,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.core.util)
|
implementation(projects.core.util)
|
||||||
implementation(projects.ui.designsystem)
|
implementation(projects.ui.designsystem)
|
||||||
|
implementation(projects.ui.sharedui)
|
||||||
implementation(projects.domain.search)
|
implementation(projects.domain.search)
|
||||||
|
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
package dev.adriankuta.flights.ui.home
|
package dev.adriankuta.flights.ui.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
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
|
||||||
@ -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.designsystem.theme.PreviewDevices
|
||||||
import dev.adriankuta.flights.ui.home.components.AirportDropdown
|
import dev.adriankuta.flights.ui.home.components.AirportDropdown
|
||||||
import dev.adriankuta.flights.ui.home.components.DatePicker
|
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
|
||||||
@ -35,6 +41,9 @@ internal fun HomeScreen(
|
|||||||
onOriginAirportSelect = viewModel::selectOriginAirport,
|
onOriginAirportSelect = viewModel::selectOriginAirport,
|
||||||
onDestinationAirportSelect = viewModel::selectDestinationAirport,
|
onDestinationAirportSelect = viewModel::selectDestinationAirport,
|
||||||
onDateSelect = viewModel::selectDate,
|
onDateSelect = viewModel::selectDate,
|
||||||
|
onAdultCountChange = viewModel::updateAdultCount,
|
||||||
|
onTeenCountChange = viewModel::updateTeenCount,
|
||||||
|
onChildCountChange = viewModel::updateChildCount,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +53,9 @@ private fun HomeScreen(
|
|||||||
onOriginAirportSelect: (AirportInfo) -> Unit,
|
onOriginAirportSelect: (AirportInfo) -> Unit,
|
||||||
onDestinationAirportSelect: (AirportInfo) -> Unit,
|
onDestinationAirportSelect: (AirportInfo) -> Unit,
|
||||||
onDateSelect: (LocalDate) -> Unit,
|
onDateSelect: (LocalDate) -> Unit,
|
||||||
|
onAdultCountChange: (Int) -> Unit,
|
||||||
|
onTeenCountChange: (Int) -> Unit,
|
||||||
|
onChildCountChange: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@ -52,7 +64,29 @@ private fun HomeScreen(
|
|||||||
when (uiState) {
|
when (uiState) {
|
||||||
is HomeUiState.Error -> Text("Error")
|
is HomeUiState.Error -> Text("Error")
|
||||||
HomeUiState.Loading -> Text("Loading")
|
HomeUiState.Loading -> Text("Loading")
|
||||||
is HomeUiState.Success -> {
|
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(
|
AirportDropdown(
|
||||||
label = "Origin Airport",
|
label = "Origin Airport",
|
||||||
airports = uiState.originAirports,
|
airports = uiState.originAirports,
|
||||||
@ -81,8 +115,44 @@ private fun HomeScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = uiState.selectedDestinationAirport != null,
|
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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +165,9 @@ private fun HomeScreenLoadingPreview() {
|
|||||||
onOriginAirportSelect = {},
|
onOriginAirportSelect = {},
|
||||||
onDestinationAirportSelect = {},
|
onDestinationAirportSelect = {},
|
||||||
onDateSelect = {},
|
onDateSelect = {},
|
||||||
|
onAdultCountChange = {},
|
||||||
|
onTeenCountChange = {},
|
||||||
|
onChildCountChange = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,10 +210,18 @@ private fun HomeScreenSuccessPreview() {
|
|||||||
selectedOriginAirport = mockAirports.first(),
|
selectedOriginAirport = mockAirports.first(),
|
||||||
selectedDestinationAirport = mockAirports.last(),
|
selectedDestinationAirport = mockAirports.last(),
|
||||||
selectedDate = LocalDate.now(),
|
selectedDate = LocalDate.now(),
|
||||||
|
passengers = PassengersState(
|
||||||
|
adultCount = 2,
|
||||||
|
teenCount = 1,
|
||||||
|
childCount = 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onOriginAirportSelect = {},
|
onOriginAirportSelect = {},
|
||||||
onDestinationAirportSelect = {},
|
onDestinationAirportSelect = {},
|
||||||
onDateSelect = {},
|
onDateSelect = {},
|
||||||
|
onAdultCountChange = {},
|
||||||
|
onTeenCountChange = {},
|
||||||
|
onChildCountChange = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import dev.adriankuta.flights.domain.types.AirportInfo
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
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 java.time.LocalDate
|
import java.time.LocalDate
|
||||||
@ -23,12 +24,20 @@ class HomeScreenViewModel @Inject constructor(
|
|||||||
private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
|
private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
|
||||||
private val selectedDestinationAirport = MutableStateFlow<AirportInfo?>(null)
|
private val selectedDestinationAirport = MutableStateFlow<AirportInfo?>(null)
|
||||||
private val selectedDate = MutableStateFlow(LocalDate.now())
|
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(
|
internal val uiState = homeUiState(
|
||||||
useCase = observeAirportsUseCase,
|
useCase = observeAirportsUseCase,
|
||||||
selectedOriginAirport = selectedOriginAirport,
|
selectedOriginAirport = selectedOriginAirport,
|
||||||
selectedDestinationAirport = selectedDestinationAirport,
|
selectedDestinationAirport = selectedDestinationAirport,
|
||||||
selectedDate = selectedDate,
|
selectedDate = selectedDate,
|
||||||
|
passengersState = passengersState(
|
||||||
|
adultCount = adultCount,
|
||||||
|
teenCount = teenCount,
|
||||||
|
childCount = childCount,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
@ -61,20 +70,37 @@ class HomeScreenViewModel @Inject constructor(
|
|||||||
fun selectDate(date: LocalDate) {
|
fun selectDate(date: LocalDate) {
|
||||||
selectedDate.value = date
|
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(
|
private fun homeUiState(
|
||||||
useCase: ObserveAirportsUseCase,
|
useCase: ObserveAirportsUseCase,
|
||||||
selectedOriginAirport: MutableStateFlow<AirportInfo?>,
|
selectedOriginAirport: StateFlow<AirportInfo?>,
|
||||||
selectedDestinationAirport: MutableStateFlow<AirportInfo?>,
|
selectedDestinationAirport: StateFlow<AirportInfo?>,
|
||||||
selectedDate: MutableStateFlow<LocalDate>,
|
selectedDate: StateFlow<LocalDate>,
|
||||||
|
passengersState: Flow<PassengersState>,
|
||||||
): Flow<HomeUiState> {
|
): Flow<HomeUiState> {
|
||||||
return combine(
|
return combine(
|
||||||
useCase().asResult(),
|
useCase().asResult(),
|
||||||
selectedOriginAirport,
|
selectedOriginAirport,
|
||||||
selectedDestinationAirport,
|
selectedDestinationAirport,
|
||||||
selectedDate,
|
selectedDate,
|
||||||
) { result, origin, destination, date ->
|
passengersState,
|
||||||
|
) { result, origin, destination, date, passengers ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Error -> HomeUiState.Error(result.exception)
|
is Result.Error -> HomeUiState.Error(result.exception)
|
||||||
is Result.Loading -> HomeUiState.Loading
|
is Result.Loading -> HomeUiState.Loading
|
||||||
@ -86,12 +112,31 @@ private fun homeUiState(
|
|||||||
selectedOriginAirport = origin,
|
selectedOriginAirport = origin,
|
||||||
selectedDestinationAirport = destination,
|
selectedDestinationAirport = destination,
|
||||||
selectedDate = date,
|
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 {
|
internal sealed interface HomeUiState {
|
||||||
data object Loading : HomeUiState
|
data object Loading : HomeUiState
|
||||||
data class Success(
|
data class Success(
|
||||||
@ -100,7 +145,14 @@ internal sealed interface HomeUiState {
|
|||||||
val selectedOriginAirport: AirportInfo? = null,
|
val selectedOriginAirport: AirportInfo? = null,
|
||||||
val selectedDestinationAirport: AirportInfo? = null,
|
val selectedDestinationAirport: AirportInfo? = null,
|
||||||
val selectedDate: LocalDate = LocalDate.now(),
|
val selectedDate: LocalDate = LocalDate.now(),
|
||||||
|
val passengers: PassengersState,
|
||||||
) : HomeUiState
|
) : HomeUiState
|
||||||
|
|
||||||
data class Error(val exception: Throwable) : 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,
|
onCountChange: (diff: Int) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
label: String? = null,
|
label: String? = null,
|
||||||
|
minVal: Int = 0,
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
|
||||||
@ -86,11 +87,12 @@ fun Counter(
|
|||||||
onCountChange(-1)
|
onCountChange(-1)
|
||||||
},
|
},
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
enabled = value > 0,
|
enabled = value > minVal,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "-",
|
text = "-",
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
@ -105,6 +107,7 @@ fun Counter(
|
|||||||
Text(
|
Text(
|
||||||
text = "+",
|
text = "+",
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user