From 8d850c72a8c80a99883be46b62fa1b4f98b686bb Mon Sep 17 00:00:00 2001 From: Adrian Kuta Date: Mon, 16 Jun 2025 09:21:57 +0200 Subject: [PATCH] Refactor: Improve flight search UI and logic This commit introduces several enhancements to the flight search user interface and underlying logic. Key changes: - Added an extension function `Flight.hasFlights()` to check if a flight object contains any actual flight data. This is located in `ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/extensions/FlightExt.kt`. - In `StationsScreen.kt`: - The origin airport display now includes a "Origin airport" label. - The search input field placeholder text dynamically changes based on whether an origin airport has been selected (e.g., "Search origin airport" vs. "Search destination airport"). - In `StationsScreenViewModel.kt`: - When an airport is selected, the search query is now automatically cleared. - Added new string resources in `ui/stations/src/main/res/values/strings.xml` for the dynamic placeholder text: `search_origin_airport` and `search_destination_airport`. - In `SearchResults.kt`: - Implemented an `EmptyListComponent` to display a user-friendly message with an icon when no flights are found for the given search criteria. - The `SearchResults` composable now utilizes `itemsIndexed` for better handling of dividers between trip cards. - Dividers are no longer shown after the last item in the list of trips or trip dates. - Added previews for the empty list state (`EmptyListPreview` and `SearchResultsEmptyPreview`). --- .../ui/home/components/SearchResults.kt | 84 +++++++++++++++++-- .../flights/ui/home/extensions/FlightExt.kt | 6 ++ .../flights/ui/home/StationsScreen.kt | 20 ++++- .../ui/home/StationsScreenViewModel.kt | 1 + ui/stations/src/main/res/values/strings.xml | 3 +- 5 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/extensions/FlightExt.kt diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/SearchResults.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/SearchResults.kt index 3f35f4d..3f9b342 100644 --- a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/SearchResults.kt +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/components/SearchResults.kt @@ -1,22 +1,28 @@ package dev.adriankuta.flights.ui.home.components +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -25,6 +31,7 @@ import dev.adriankuta.flights.domain.types.Trip import dev.adriankuta.flights.domain.types.TripDate import dev.adriankuta.flights.domain.types.TripFlight import dev.adriankuta.flights.ui.home.SearchResultUiState +import dev.adriankuta.flights.ui.home.extensions.hasFlights import dev.adriankuta.flights.ui.home.sample.FlightPreviewParameterProvider @Composable @@ -48,9 +55,17 @@ internal fun SearchResults( is SearchResultUiState.Error -> item { Text("Error") } SearchResultUiState.Idle -> Unit SearchResultUiState.Loading -> item { Text("Loading") } - is SearchResultUiState.Success -> items(uiState.flight.trips) { trip -> - TripCard(trip = trip, currency = uiState.flight.currency) - Spacer(modifier = Modifier.height(16.dp)) + is SearchResultUiState.Success -> { + if (uiState.flight.hasFlights()) { + itemsIndexed(uiState.flight.trips) { index, trip -> + TripCard(trip = trip, currency = uiState.flight.currency) + if (index < uiState.flight.trips.lastIndex) { + Spacer(modifier = Modifier.height(16.dp)) + } + } + } else { + item { EmptyListComponent() } + } } } } @@ -78,9 +93,11 @@ private fun TripCard( Spacer(modifier = Modifier.height(8.dp)) Column { - trip.dates.forEach { tripDate -> + trip.dates.forEachIndexed { index, tripDate -> TripDateItem(tripDate = tripDate, currency = currency) - HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + if (index != trip.dates.lastIndex) { + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + } } } } @@ -163,8 +180,63 @@ private fun FlightItem( } } +@Composable +private fun EmptyListComponent( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + modifier = Modifier.padding(bottom = 16.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = "No flights found", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Try changing your search criteria", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun EmptyListPreview() { + EmptyListComponent() +} + @Preview(showBackground = true) @Composable private fun SearchResultsPreview(@PreviewParameter(FlightPreviewParameterProvider::class) flight: Flight) { SearchResults(uiState = SearchResultUiState.Success(flight)) } + +@Preview(showBackground = true) +@Composable +private fun SearchResultsEmptyPreview() { + SearchResults( + uiState = SearchResultUiState.Success( + Flight( + currency = "EUR", + currPrecision = 2, + trips = emptyList(), + ), + ), + ) +} diff --git a/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/extensions/FlightExt.kt b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/extensions/FlightExt.kt new file mode 100644 index 0000000..12d0505 --- /dev/null +++ b/ui/home/src/main/kotlin/dev/adriankuta/flights/ui/home/extensions/FlightExt.kt @@ -0,0 +1,6 @@ +package dev.adriankuta.flights.ui.home.extensions + +import dev.adriankuta.flights.domain.types.Flight + +internal fun Flight.hasFlights(): Boolean = + trips.firstOrNull()?.dates?.firstOrNull()?.flights?.isNotEmpty() == true diff --git a/ui/stations/src/main/kotlin/dev/adriankuta/flights/ui/home/StationsScreen.kt b/ui/stations/src/main/kotlin/dev/adriankuta/flights/ui/home/StationsScreen.kt index affb149..4677954 100644 --- a/ui/stations/src/main/kotlin/dev/adriankuta/flights/ui/home/StationsScreen.kt +++ b/ui/stations/src/main/kotlin/dev/adriankuta/flights/ui/home/StationsScreen.kt @@ -71,9 +71,21 @@ private fun StationsScreen( LaunchedEffect(selectedOriginAirport) { selectedOriginAirport?.let { airportInfo = it } } - AirportInfoCompact( - data = airportInfo, - ) + Column { + Text( + text = "Origin airport", + modifier = Modifier.padding(horizontal = 16.dp), + ) + AirportInfoCompact( + data = airportInfo, + ) + } + } + + val hintRes = if (selectedOriginAirport == null) { + R.string.search_origin_airport + } else { + R.string.search_destination_airport } OutlinedTextField( @@ -82,7 +94,7 @@ private fun StationsScreen( modifier = Modifier .fillMaxWidth() .padding(16.dp), - placeholder = { Text(text = stringResource(R.string.search_airports)) }, + placeholder = { Text(text = stringResource(hintRes)) }, leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, singleLine = true, ) diff --git a/ui/stations/src/main/kotlin/dev/adriankuta/flights/ui/home/StationsScreenViewModel.kt b/ui/stations/src/main/kotlin/dev/adriankuta/flights/ui/home/StationsScreenViewModel.kt index e702bec..1126cec 100644 --- a/ui/stations/src/main/kotlin/dev/adriankuta/flights/ui/home/StationsScreenViewModel.kt +++ b/ui/stations/src/main/kotlin/dev/adriankuta/flights/ui/home/StationsScreenViewModel.kt @@ -45,6 +45,7 @@ internal class StationsScreenViewModel @Inject constructor( fun onAirportSelected(airport: AirportInfo) { _selectedOriginAirport.value = airport + _searchQuery.value = "" } fun clearSelectedAirport() { diff --git a/ui/stations/src/main/res/values/strings.xml b/ui/stations/src/main/res/values/strings.xml index 11281cb..cbb881f 100644 --- a/ui/stations/src/main/res/values/strings.xml +++ b/ui/stations/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ Stations - Search airports + Search origin airport + Search destination airport