mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-07-02 02:07:58 +02:00
feat: Implement origin and destination airport selection
This commit introduces functionality for selecting origin and destination airports on the home screen. Key changes: - Modified `HomeScreen` to use `Column` instead of `LazyColumn` for layout. - Added `AirportDropdown` composable component for selecting airports. - Updated `HomeScreen` to include two `AirportDropdown` instances: one for origin and one for destination. - Enhanced `HomeScreenViewModel`: - Added `selectedOriginAirport` and `selectedDestinationAirport` state flows. - Implemented `selectOriginAirport` and `selectDestinationAirport` functions to update selected airports. - Added `clearOriginAirport` and `clearDestinationAirport` functions. - Updated `homeUiState` to combine airport data with selected airports and filter destination airports to exclude the selected origin. - Updated `HomeUiState.Success` to include `originAirports`, `destinationAirports`, `selectedOriginAirport`, and `selectedDestinationAirport`. - Added new preview functions for `HomeScreen` in loading and success states with mock data.
This commit is contained in:
@ -1,15 +1,26 @@
|
|||||||
package dev.adriankuta.flights.ui.home
|
package dev.adriankuta.flights.ui.home
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.lazy.items
|
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.Text
|
||||||
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
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||||
|
import dev.adriankuta.flights.domain.types.City
|
||||||
|
import dev.adriankuta.flights.domain.types.Coordinates
|
||||||
|
import dev.adriankuta.flights.domain.types.Country
|
||||||
|
import dev.adriankuta.flights.domain.types.MacCity
|
||||||
|
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
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HomeScreen(
|
internal fun HomeScreen(
|
||||||
@ -19,22 +30,43 @@ internal fun HomeScreen(
|
|||||||
|
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
uiState = homeUiState,
|
uiState = homeUiState,
|
||||||
|
onOriginAirportSelect = viewModel::selectOriginAirport,
|
||||||
|
onDestinationAirportSelect = viewModel::selectDestinationAirport,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HomeScreen(
|
private fun HomeScreen(
|
||||||
uiState: HomeUiState,
|
uiState: HomeUiState,
|
||||||
|
onOriginAirportSelect: (AirportInfo) -> Unit,
|
||||||
|
onDestinationAirportSelect: (AirportInfo) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
when (uiState) {
|
when (uiState) {
|
||||||
is HomeUiState.Error -> item { Text("Error") }
|
is HomeUiState.Error -> Text("Error")
|
||||||
HomeUiState.Loading -> item { Text("Loading") }
|
HomeUiState.Loading -> Text("Loading")
|
||||||
is HomeUiState.Success -> items(uiState.airports) { airport ->
|
is HomeUiState.Success -> {
|
||||||
Text(airport.name)
|
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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,10 +74,56 @@ private fun HomeScreen(
|
|||||||
|
|
||||||
@PreviewDevices
|
@PreviewDevices
|
||||||
@Composable
|
@Composable
|
||||||
private fun HomeScreenPreview() {
|
private fun HomeScreenLoadingPreview() {
|
||||||
FlightsTheme {
|
FlightsTheme {
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
uiState = HomeUiState.Loading,
|
uiState = HomeUiState.Loading,
|
||||||
|
onOriginAirportSelect = {},
|
||||||
|
onDestinationAirportSelect = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewDevices
|
||||||
|
@Composable
|
||||||
|
private fun HomeScreenSuccessPreview() {
|
||||||
|
FlightsTheme {
|
||||||
|
val mockAirports = listOf(
|
||||||
|
AirportInfo(
|
||||||
|
code = "WAW",
|
||||||
|
name = "Warsaw Chopin Airport",
|
||||||
|
seoName = "warsaw",
|
||||||
|
isBase = true,
|
||||||
|
timeZone = "Europe/Warsaw",
|
||||||
|
city = City("WAW", "Warsaw"),
|
||||||
|
macCity = MacCity("WARSAW"),
|
||||||
|
region = Region("WARSAW_PL", "Warsaw"),
|
||||||
|
country = Country("PL", "Poland", "PLN"),
|
||||||
|
coordinates = Coordinates(52.1657, 20.9671),
|
||||||
|
),
|
||||||
|
AirportInfo(
|
||||||
|
code = "KRK",
|
||||||
|
name = "Krakow Airport",
|
||||||
|
seoName = "krakow",
|
||||||
|
isBase = true,
|
||||||
|
timeZone = "Europe/Warsaw",
|
||||||
|
city = City("KRK", "Krakow"),
|
||||||
|
macCity = MacCity("KRAKOW"),
|
||||||
|
region = Region("KRAKOW_PL", "Krakow"),
|
||||||
|
country = Country("PL", "Poland", "PLN"),
|
||||||
|
coordinates = Coordinates(50.0777, 19.7848),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
HomeScreen(
|
||||||
|
uiState = HomeUiState.Success(
|
||||||
|
originAirports = mockAirports,
|
||||||
|
destinationAirports = mockAirports,
|
||||||
|
selectedOriginAirport = mockAirports.first(),
|
||||||
|
selectedDestinationAirport = null,
|
||||||
|
),
|
||||||
|
onOriginAirportSelect = {},
|
||||||
|
onDestinationAirportSelect = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,9 @@ import dev.adriankuta.flights.core.util.asResult
|
|||||||
import dev.adriankuta.flights.domain.search.ObserveAirportsUseCase
|
import dev.adriankuta.flights.domain.search.ObserveAirportsUseCase
|
||||||
import dev.adriankuta.flights.domain.types.AirportInfo
|
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.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -18,32 +19,77 @@ class HomeScreenViewModel @Inject constructor(
|
|||||||
private val observeAirportsUseCase: ObserveAirportsUseCase,
|
private val observeAirportsUseCase: ObserveAirportsUseCase,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val selectedOriginAirport = MutableStateFlow<AirportInfo?>(null)
|
||||||
|
private val selectedDestinationAirport = MutableStateFlow<AirportInfo?>(null)
|
||||||
|
|
||||||
internal val uiState = homeUiState(
|
internal val uiState = homeUiState(
|
||||||
useCase = observeAirportsUseCase,
|
useCase = observeAirportsUseCase,
|
||||||
|
selectedOriginAirport = selectedOriginAirport,
|
||||||
|
selectedDestinationAirport = selectedDestinationAirport,
|
||||||
)
|
)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.WhileSubscribed(5_000),
|
started = SharingStarted.WhileSubscribed(5_000),
|
||||||
initialValue = HomeUiState.Loading,
|
initialValue = HomeUiState.Loading,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun selectOriginAirport(airport: AirportInfo) {
|
||||||
|
selectedOriginAirport.value = airport
|
||||||
|
// If the selected destination is the same as the new origin, clear the destination
|
||||||
|
if (selectedDestinationAirport.value?.code == airport.code) {
|
||||||
|
selectedDestinationAirport.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectDestinationAirport(airport: AirportInfo) {
|
||||||
|
selectedDestinationAirport.value = airport
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearOriginAirport() {
|
||||||
|
selectedOriginAirport.value = null
|
||||||
|
// Clear destination as well since it depends on origin
|
||||||
|
selectedDestinationAirport.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearDestinationAirport() {
|
||||||
|
selectedDestinationAirport.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun homeUiState(
|
private fun homeUiState(
|
||||||
useCase: ObserveAirportsUseCase,
|
useCase: ObserveAirportsUseCase,
|
||||||
|
selectedOriginAirport: MutableStateFlow<AirportInfo?>,
|
||||||
|
selectedDestinationAirport: MutableStateFlow<AirportInfo?>,
|
||||||
): Flow<HomeUiState> {
|
): Flow<HomeUiState> {
|
||||||
return useCase()
|
return combine(
|
||||||
.asResult()
|
useCase().asResult(),
|
||||||
.map { result ->
|
selectedOriginAirport,
|
||||||
|
selectedDestinationAirport,
|
||||||
|
) { result, origin, destination ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Error -> HomeUiState.Error(result.exception)
|
is Result.Error -> HomeUiState.Error(result.exception)
|
||||||
Result.Loading -> HomeUiState.Loading
|
is Result.Loading -> HomeUiState.Loading
|
||||||
is Result.Success -> HomeUiState.Success(result.data.orEmpty())
|
is Result.Success -> {
|
||||||
|
val airports = result.data.orEmpty()
|
||||||
|
HomeUiState.Success(
|
||||||
|
originAirports = airports,
|
||||||
|
destinationAirports = airports.filter { it.code != origin?.code },
|
||||||
|
selectedOriginAirport = origin,
|
||||||
|
selectedDestinationAirport = destination,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed interface HomeUiState {
|
internal sealed interface HomeUiState {
|
||||||
data object Loading : HomeUiState
|
data object Loading : HomeUiState
|
||||||
data class Success(val airports: List<AirportInfo>) : HomeUiState
|
data class Success(
|
||||||
|
val originAirports: List<AirportInfo>,
|
||||||
|
val destinationAirports: List<AirportInfo>,
|
||||||
|
val selectedOriginAirport: AirportInfo? = null,
|
||||||
|
val selectedDestinationAirport: AirportInfo? = null,
|
||||||
|
) : HomeUiState
|
||||||
|
|
||||||
data class Error(val exception: Throwable) : HomeUiState
|
data class Error(val exception: Throwable) : HomeUiState
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
package dev.adriankuta.flights.ui.home.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.MenuAnchorType
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import dev.adriankuta.flights.domain.types.AirportInfo
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AirportDropdown(
|
||||||
|
label: String,
|
||||||
|
airports: List<AirportInfo>,
|
||||||
|
selectedAirport: AirportInfo?,
|
||||||
|
onAirportSelect: (AirportInfo) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { if (enabled) expanded = it },
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selectedAirport?.let { "${it.name} (${it.code})" } ?: "",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text(label) },
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||||
|
},
|
||||||
|
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(MenuAnchorType.PrimaryNotEditable, enabled),
|
||||||
|
enabled = enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
) {
|
||||||
|
airports.forEach { airport ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("${airport.name} (${airport.code})") },
|
||||||
|
onClick = {
|
||||||
|
onAirportSelect(airport)
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user