diff --git a/.jules/bolt.md b/.jules/bolt.md index becccd2..b3ac245 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -4,3 +4,6 @@ ## 2024-05-19 - Replacing pre-sized ArrayList + for loop with List(size) constructor **Learning:** In Kotlin, creating a `List` using the functional constructor `List(size) { index -> ... }` can be slightly faster and is definitely cleaner than manually sizing an `ArrayList` and using a `for` loop to `.add()` items, even when the `ArrayList` is pre-sized. **Action:** Default to the `List(size) { ... }` constructor when mapping indexed access (like from an Android framework class or external API that doesn't provide an Iterator) into a Kotlin List. +## 2025-01-31 - Debouncing and Offloading Compose UI Thread Computations +**Learning:** Performing expensive filtering operations (e.g. searching through thousands of local tracks) directly in Compose's `remember` block without debouncing can block the main thread and cause UI stuttering, especially on older devices. +**Action:** Use `LaunchedEffect` with a small `delay(200)` to debounce user input. Additionally, wrap the expensive collection filtering inside `withContext(Dispatchers.Default)` to ensure the computation runs on a background thread instead of blocking the main thread. diff --git a/app/src/main/java/com/rockmusic/app/presentation/RockMusicRoot.kt b/app/src/main/java/com/rockmusic/app/presentation/RockMusicRoot.kt index 53f065a..a17bce6 100644 --- a/app/src/main/java/com/rockmusic/app/presentation/RockMusicRoot.kt +++ b/app/src/main/java/com/rockmusic/app/presentation/RockMusicRoot.kt @@ -96,7 +96,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.rockmusic.app.domain.model.LocalTrack import com.rockmusic.app.player.PlayerUiState +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext private enum class Destination(val label: String, val icon: ImageVector) { Home("Home", Icons.Rounded.Home), @@ -478,14 +480,22 @@ private fun HomeScreen( @Composable private fun SearchScreen(tracks: List, onPlay: (LocalTrack) -> Unit) { var query by remember { mutableStateOf("") } - val results = remember(query, tracks) { + var results by remember { mutableStateOf>(emptyList()) } + + LaunchedEffect(query, tracks) { if (query.isBlank()) { - emptyList() + results = emptyList() } else { - tracks.filter { - it.title.contains(query, true) || - it.artist.contains(query, true) || - it.album.contains(query, true) + delay(200) // ⚡ Bolt: debounce input to prevent frequent updates + val currentQuery = query + val currentTracks = tracks + results = withContext(Dispatchers.Default) { + // ⚡ Bolt: filter on background thread to prevent UI stutter + currentTracks.filter { + it.title.contains(currentQuery, true) || + it.artist.contains(currentQuery, true) || + it.album.contains(currentQuery, true) + } } } }