mirror of
https://github.com/AdrianKuta/android-challange-adrian-kuta.git
synced 2025-07-01 14:47:59 +02:00
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`).
This commit is contained in:
@ -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,11 +55,19 @@ 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 ->
|
||||
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,14 +93,16 @@ private fun TripCard(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Column {
|
||||
trip.dates.forEach { tripDate ->
|
||||
trip.dates.forEachIndexed { index, tripDate ->
|
||||
TripDateItem(tripDate = tripDate, currency = currency)
|
||||
if (index != trip.dates.lastIndex) {
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TripDateItem(
|
||||
@ -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(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -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
|
@ -71,10 +71,22 @@ private fun StationsScreen(
|
||||
LaunchedEffect(selectedOriginAirport) {
|
||||
selectedOriginAirport?.let { airportInfo = it }
|
||||
}
|
||||
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(
|
||||
value = searchQuery,
|
||||
@ -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,
|
||||
)
|
||||
|
@ -45,6 +45,7 @@ internal class StationsScreenViewModel @Inject constructor(
|
||||
|
||||
fun onAirportSelected(airport: AirportInfo) {
|
||||
_selectedOriginAirport.value = airport
|
||||
_searchQuery.value = ""
|
||||
}
|
||||
|
||||
fun clearSelectedAirport() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="stations_screen_title">Stations</string>
|
||||
<string name="search_airports">Search airports</string>
|
||||
<string name="search_origin_airport">Search origin airport</string>
|
||||
<string name="search_destination_airport">Search destination airport</string>
|
||||
</resources>
|
||||
|
Reference in New Issue
Block a user