mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-07-02 02:07:58 +02: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