From c245f75ca1e85685a5c5381f3b8126a4184bf939 Mon Sep 17 00:00:00 2001 From: rahul31124 Date: Sun, 26 Jan 2025 13:52:44 +0530 Subject: [PATCH 1/3] UI State Loss In BrainzPlayerSearchScreen --- .../search/BrainzPlayerSearchScreen.kt | 39 ++++++++++--------- .../viewmodel/BrainzPlayerViewModel.kt | 34 ++++++++++++---- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt index 744d81d4..ee18df98 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf @@ -26,6 +27,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue import androidx.hilt.navigation.compose.hiltViewModel import org.listenbrainz.android.R import org.listenbrainz.android.model.PlayableType @@ -46,21 +48,15 @@ fun BrainzPlayerSearchScreen( deactivate: () -> Unit, ) { val context = LocalContext.current - var brainzplayerQueryState by remember { - mutableStateOf("") - } - - val searchItems = remember { - mutableStateListOf() - } + val brainzplayerQueryState by viewModel.searchQuery.collectAsState() + val searchItems by viewModel.searchItems.collectAsState() var error by remember { mutableStateOf(null) } fun onDismiss() { - searchItems.clear() - brainzplayerQueryState = "" + viewModel.clearSearchResults() error = null deactivate() } @@ -71,20 +67,22 @@ fun BrainzPlayerSearchScreen( exit = fadeOut() ) { SearchScreen( - uiState = remember(searchItems, brainzplayerQueryState, error) { + uiState = remember(searchItems, brainzplayerQueryState.text, error) { SearchUiState( - query = brainzplayerQueryState, + query = brainzplayerQueryState.text, result = searchItems, error = error ) }, onDismiss = ::onDismiss, - onQueryChange = { newValue -> - brainzplayerQueryState = newValue - searchItems.clear() - searchItems.addAll(viewModel.searchSongs(brainzplayerQueryState) ?: emptyList()) + onQueryChange = { newValue: String -> + val updatedQuery = TextFieldValue(newValue, selection = brainzplayerQueryState.selection) + viewModel.updateSearchQuery(updatedQuery) + viewModel.searchSongs(newValue) + }, + onClear = { + viewModel.clearSearchResults() }, - onClear = searchItems::clear, onErrorShown = { error = null }, placeholderText = "Search your music library" ) { @@ -102,7 +100,12 @@ fun BrainzPlayerSearchScreen( onDropdownSuccess = { context.showToast(it) }, onDropdownError = { error = it } ) { - viewModel.changePlayable(listOf(song), PlayableType.SONG, song.mediaID, 0) + viewModel.changePlayable( + listOf(song), + PlayableType.SONG, + song.mediaID, + 0 + ) viewModel.playOrToggleSong(song, true) onDismiss() } @@ -110,4 +113,4 @@ fun BrainzPlayerSearchScreen( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt index d4890ce9..afc2b197 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.palette.graphics.Palette @@ -23,6 +24,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -191,19 +193,37 @@ class BrainzPlayerViewModel @Inject constructor( } } - fun searchSongs(query: String): List? { - val listToSearch = _mediaItems.value.data + private val _searchQuery = MutableStateFlow(TextFieldValue("")) + val searchQuery: StateFlow = _searchQuery + + private val _searchItems = MutableStateFlow>(emptyList()) + val searchItems: StateFlow> = _searchItems + + fun updateSearchQuery(newQuery: TextFieldValue) { + _searchQuery.value = newQuery + } + fun searchSongs(query: String) { + val listToSearch = _mediaItems.value.data if (query.isEmpty()) { isSearching = false + _searchItems.value = emptyList() + } else { + val result: List? = listToSearch?.filter { + it.title.contains(query.trim(), ignoreCase = true) + } + _searchItems.value = result ?: emptyList() + isSearching = true } - val result: List? = listToSearch?.filter { - it.title.contains(query.trim(), ignoreCase = true) - } - isSearching = true - return result } + fun clearSearchResults() { + _searchItems.value = emptyList() + _searchQuery.value = TextFieldValue("") + } + + + fun playOrToggleSong(mediaItem: Song, toggle: Boolean = false) { val isPrepared = playbackState.value.isPrepared if (isPrepared && mediaItem.mediaID == currentlyPlayingSong.value.toSong.mediaID) { From f8e51055abdbe53d9fdda8634e552a3b80280bef Mon Sep 17 00:00:00 2001 From: rahul31124 Date: Sun, 26 Jan 2025 22:21:30 +0530 Subject: [PATCH 2/3] Move search logic to ViewModel with debounce --- .../search/BrainzPlayerSearchScreen.kt | 1 - .../viewmodel/BrainzPlayerViewModel.kt | 40 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt index ee18df98..103b5b90 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/search/BrainzPlayerSearchScreen.kt @@ -78,7 +78,6 @@ fun BrainzPlayerSearchScreen( onQueryChange = { newValue: String -> val updatedQuery = TextFieldValue(newValue, selection = brainzplayerQueryState.selection) viewModel.updateSearchQuery(updatedQuery) - viewModel.searchSongs(newValue) }, onClear = { viewModel.clearSearchResults() diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt index afc2b197..8561ddea 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -27,6 +27,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.listenbrainz.android.model.Playable import org.listenbrainz.android.model.PlayableType @@ -73,6 +76,12 @@ class BrainzPlayerViewModel @Inject constructor( var playerBackGroundColor by mutableStateOf(Color.Transparent) + private val _searchQuery = MutableStateFlow(TextFieldValue("")) + val searchQuery: StateFlow = _searchQuery + + private val _searchItems = MutableStateFlow>(emptyList()) + val searchItems: StateFlow> = _searchItems + init { updatePlayerPosition() _mediaItems.value = Resource.loading() @@ -98,6 +107,16 @@ class BrainzPlayerViewModel @Inject constructor( currentlyPlaying.items.plus(it) } } + + viewModelScope.launch { + _searchQuery + .map { it.text } + .debounce(200) // 0.2 Seconds + .distinctUntilChanged() + .collect { query -> + SearchSong(query) + } + } } fun updateBackgroundColorForPlayer( @@ -193,28 +212,22 @@ class BrainzPlayerViewModel @Inject constructor( } } - private val _searchQuery = MutableStateFlow(TextFieldValue("")) - val searchQuery: StateFlow = _searchQuery - - private val _searchItems = MutableStateFlow>(emptyList()) - val searchItems: StateFlow> = _searchItems - fun updateSearchQuery(newQuery: TextFieldValue) { _searchQuery.value = newQuery } - fun searchSongs(query: String) { + private fun SearchSong(query: String) { val listToSearch = _mediaItems.value.data if (query.isEmpty()) { isSearching = false _searchItems.value = emptyList() - } else { - val result: List? = listToSearch?.filter { - it.title.contains(query.trim(), ignoreCase = true) - } - _searchItems.value = result ?: emptyList() - isSearching = true + return } + isSearching = true + val result: List? = listToSearch?.filter { + it.title.contains(query.trim(), ignoreCase = true) + } + _searchItems.value = result ?: emptyList() } fun clearSearchResults() { @@ -223,7 +236,6 @@ class BrainzPlayerViewModel @Inject constructor( } - fun playOrToggleSong(mediaItem: Song, toggle: Boolean = false) { val isPrepared = playbackState.value.isPrepared if (isPrepared && mediaItem.mediaID == currentlyPlayingSong.value.toSong.mediaID) { From 3894327102090bf6207d3a4e05d46dbf8eda2511 Mon Sep 17 00:00:00 2001 From: Jasjeet Singh <98077881+07jasjeet@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:31:40 +0530 Subject: [PATCH 3/3] Remove `isSearching` variable --- .../listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt index 8561ddea..2f430a1e 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/BrainzPlayerViewModel.kt @@ -72,7 +72,6 @@ class BrainzPlayerViewModel @Inject constructor( val isPlaying = brainzPlayerServiceConnection.isPlaying val playButton = brainzPlayerServiceConnection.playButtonState val repeatMode = brainzPlayerServiceConnection.repeatModeState - var isSearching by mutableStateOf(false) var playerBackGroundColor by mutableStateOf(Color.Transparent) @@ -219,11 +218,9 @@ class BrainzPlayerViewModel @Inject constructor( private fun SearchSong(query: String) { val listToSearch = _mediaItems.value.data if (query.isEmpty()) { - isSearching = false _searchItems.value = emptyList() return } - isSearching = true val result: List? = listToSearch?.filter { it.title.contains(query.trim(), ignoreCase = true) }