Anyone want to do code review a search field with ...
# compose
c
Anyone want to do code review a search field with "debounce"? My teammate is unsure of the solution that she wrote, but I think it looks fine?
Copy code
@HiltViewModel
class MyViewModel @Inject constructor(val appState: AppStateHolder) : ViewModel() {
  val searchText = MutableStateFlow("")

  init { viewModelScope.launch { searchText.debounce(1000).collectLatest { doSearch(it) } } }

  fun updateSearchText(text: String) { searchText.update { text } }

  fun doSearch() {...}
}
Copy code
BasicTextField(value = searchText, onValueChange = { viewModel.updateSearchText(it) } ...
g
I think it’s ok too. Normally I go like this:
Copy code
fun <T> debounce(waitMs: Long = 700L, scope: CoroutineScope = CoroutineScope(Dispatchers.Main), action: (T) -> Unit): (T) -> Unit {
    var debounceJob: Job? = null
    return { param: T ->
        debounceJob?.cancel()
        debounceJob = scope.launch {
            delay(waitMs)
            action(param)
        }
    }
}
Copy code
val updateDebounce = remember { debounce<Unit> { onFieldsUpdated(...) } }
Same logic If i want throttles:
Copy code
fun <T> throttleFirst(skipMs: Long = 700L, scope: CoroutineScope = CoroutineScope(Dispatchers.Main), action: (T) -> Unit): (T) -> Unit {
    var throttleJob: Job? = null
    return { param: T ->
        if (throttleJob == null || (throttleJob as Job).isCompleted) {
            throttleJob = scope.launch {
                action(param)
                delay(skipMs)
            }
        }
    }
}
ViewModel:
Copy code
private val loadThrottle = throttleFirst<Unit>(1000L, scope = viewModelScope) { nextPage() }

fun loadMore() {
    loadThrottle(Unit)
}
But honestly, maybe I’m over engineering…. I came up with this solution in LiveData era
c
@Colton Idle the way she’s doing it is good 👍
u
Probably you want your search to be a suspend function and if so, then use a flatMapLatest and call it
c
Interesting. I will give that a go
g
Depends on your case but I like when it purely reactive, so you just map searchText with debounce to some Flow with search result, so nooo init, no need to have state on level of VM
c
Hm. If you get a chance to write out how that would work that'd be appreciated, but I will try to get this working myself later today. That's an interesting approach i didn't consider
FWIW @ursus suggestion of making search a suspend function seemed to be required and I internally inside of search had to remove my viewModelScope.launch or else nothing would actually ever be cancelled. flatMapLatest didn't seem necessary as everything worked when I had collectLatest after I updated search to be a proper suspend function.
I tried Andreys suggestion, but couldn't really come up with anything that worked. /shruggie
u
sure flatmaplatest or collectlatest will do the same thing, but I dont like "logic" to be in collects, collect is end of reactive world in my eyes. therefore flatmaplatest is more appropriate
c
hm. I don't think I follow 100% in terms of what you mean by logic being in collects (as I see flatMap as a type of collect) but I also suck at flows so I appreciate you teaching me.
u
semantically subscribe is where reactive world ends kotlin flow made it blurry by having suspend lambda in collect 🤷‍♂️
Copy code
stream
   .collect {
      val foo = it + 5
      print(foo)
   }
vs
Copy code
stream
   .map { it + 5 }
   .collect {
      print(it)
   }
anyways the issue is this, outout is the same, but the 2nd is correct semantically
c
Thanks for the example. that helped a lot and makes sense in terms of what you meant by "logic"
c
Colton here’s something of an example of what I think people are getting at
Copy code
class MyViewModel : ViewModel() {
  val searchValue = MutableStateFlow("")
  val results = searchValue
    .debounce(1000)
    .mapLatest { doSearch(it) }

  suspend fun doSearch(query: String): List<Any> {
    // Do your search here
    return emptyList()
  }
}

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
  val searchQuery by myViewModel.searchValue.collectAsState()
  val searchResults by myViewModel.results.collectAsState(initial = emptyList())

  TextField(
    value = searchQuery,
    onValueChange = {
      myViewModel.searchValue.value = it
    },
  )

  searchResults.forEach {
    // Show results in UI here
  }
}
In the example I’ve got
doSearch
just returning
List<Any>
but of course this should return your business result type
c
Interesttting. Let em try that out!
g
yep, exactly as Chris said, also doSearch can be private and I usually hide mutability of searchValue expose Flow/StateFlow only + method like in your example updateSearchText