Skip to content

Commit

Permalink
UI: Improve stop search results handling and animations (#334)
Browse files Browse the repository at this point in the history
### TL;DR
Enhanced the stop search functionality with improved error handling and smoother UI transitions.

### What changed?
- Added key-based list item rendering for better performance
- Implemented a 1-second delay before showing "No match found" message
- Added tracking of query timestamps to prevent false "No match found" displays
- Improved error state handling with distinct keys for error messages
- Removed redundant Unit placeholder for empty state

### How to test?
1. Open the stop search screen
2. Enter a search query
3. Verify that "No match found" appears after 1 second if no results
4. Enter a valid stop name and confirm results appear
5. Clear the search and verify smooth transitions
6. Test with network errors to verify error message display


### Screenshots

https://github.com/user-attachments/assets/881f2270-fe2b-4645-b2c5-7d4ba6a648c5


### Why make this change?
To provide a more polished user experience by eliminating UI flicker, improving state transitions, and ensuring more reliable error handling when searching for stops.
  • Loading branch information
ksharma-xyz authored Nov 11, 2024
1 parent 00e1e14 commit c7be280
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
Expand Down Expand Up @@ -80,10 +82,23 @@ fun SearchStopScreen(
}

var displayNoMatchFound by remember { mutableStateOf(false) }
LaunchedEffect(searchStopState.stops.isEmpty()) {
if (!searchStopState.isLoading && textFieldText.isNotBlank() && searchStopState.stops.isEmpty()) {
delay(500)
displayNoMatchFound = true
var lastQueryTime by remember { mutableLongStateOf(0L) }
LaunchedEffect(
key1 = textFieldText,
key2 = searchStopState.stops,
key3 = searchStopState.isLoading,
) {
if (textFieldText.isNotBlank() && searchStopState.stops.isEmpty()) {
// To ensure a smooth transition from the results state to the "No match found" state,
// track the time of the last query. If new results come in during the delay period,
// then lastQueryTime will be different, therefore, it will prevent
// "No match found" message from being displayed.
val currentQueryTime = System.currentTimeMillis()
lastQueryTime = currentQueryTime
delay(1000)
if (lastQueryTime == currentQueryTime && searchStopState.stops.isEmpty()) {
displayNoMatchFound = true
}
} else {
displayNoMatchFound = false
}
Expand Down Expand Up @@ -123,7 +138,7 @@ fun SearchStopScreen(
contentPadding = PaddingValues(top = 16.dp, bottom = 48.dp),
) {
if (searchStopState.isError && textFieldText.isNotBlank()) {
item {
item(key = "Error") {
ErrorMessage(
title = "Eh! That's not looking right mate.",
message = "Let's try searching again.",
Expand All @@ -133,27 +148,27 @@ fun SearchStopScreen(
)
}
} else if (searchStopState.stops.isNotEmpty() && textFieldText.isNotBlank()) {
searchStopState.stops.forEach { stop ->
item {
StopSearchListItem(
stopId = stop.stopId,
stopName = stop.stopName,
transportModeSet = stop.transportModeType.toImmutableSet(),
textColor = KrailTheme.colors.label,
onClick = { stopItem ->
keyboard?.hide()
onStopSelect(stopItem)
},
modifier = Modifier
.fillMaxWidth()
.animateItem(),
)
items(
items = searchStopState.stops,
key = { it.stopId },
) { stop ->
StopSearchListItem(
stopId = stop.stopId,
stopName = stop.stopName,
transportModeSet = stop.transportModeType.toImmutableSet(),
textColor = KrailTheme.colors.label,
onClick = { stopItem ->
keyboard?.hide()
onStopSelect(stopItem)
},
modifier = Modifier
.fillMaxWidth(),
)

Divider()
}
Divider()
}
} else if (displayNoMatchFound && textFieldText.isNotBlank()) {
item {
item(key = "no_match") {
ErrorMessage(
title = "No match found!",
message = if (textFieldText.length < 4) {
Expand All @@ -166,8 +181,6 @@ fun SearchStopScreen(
.animateItem(),
)
}
} else {
Unit
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class SearchStopViewModel @Inject constructor(
viewModelScope.launch {
tripPlanningRepository.stopFinder(stopSearchQuery = query)
.onSuccess { response: StopFinderResponse ->
updateUiState { displayData(response.toStopResults()) }
val results = response.toStopResults()
updateUiState { displayData(results) }
}.onFailure {
updateUiState { displayError() }
}
Expand Down

0 comments on commit c7be280

Please sign in to comment.