Colton Idle
11/15/2022, 6:11 PMclass VM : ViewModel() {
val text = MutableStateFlow("")
val dbText = text.debounce(2000).distinctUntilChanged().flatMapLatest { (it) }
private fun queryFromDb(query: String): Flow<String> {
...
}
}
Colton Idle
11/15/2022, 6:11 PMColumn {
val text by viewModel.text.collectAsState()
val dbText by viewModel.dbText.collectAsState("Empty Result")
TextField(value = text, onValueChange = { viewModel.text.value = it })
Text(text = dbText)
}
Casey Brooks
11/15/2022, 6:13 PM.sample()
is probably more appropriate for this use-caseFrancesc
11/15/2022, 7:39 PMdistinctUntilChanged
because you use a StateFlow
and the flatmapLatest
doesn't do anythingFrancesc
11/15/2022, 7:41 PMclass VM : ViewModel {
private val text = MutableStateFlow("")
fun onQueryUpdate(query: String) {
text.value = query
}
}
Chris Fillmore
11/16/2022, 2:49 AMdistinctUntilChanged
, because it is applied after debounce
. If the value of a StateFlow were to change eg from “a” to “b” and back to “a” within the debounce period, this would lead to duplicate sequential emissions downstream.Chris Fillmore
11/16/2022, 2:54 AMflatMapLatest
. The lambda passed should itself be returning a Flow but in your code it seems to be returning a string (you should see an error in your editor)Chris Fillmore
11/16/2022, 2:56 AMChris Fillmore
11/16/2022, 2:57 AMColton Idle
11/16/2022, 3:26 AMCasey Brooks
11/16/2022, 3:33 AM.sample(2.seconds)
should be all you need. debounce is for waiting a minimum amount of time between emissions, for things like preventing accidental double-clicking. .sample()
emits 1 value at most every N seconds, and gives you the latest upstream value during the sampliing period. So when it eventually emits downstream to run the DB query, it will be the latest value entered by the userCasey Brooks
11/16/2022, 3:35 AM.sample()
operator for undo/redo functionality on a textfield, which works basically how you need for running the DB query. You can play around with the example to see if it gives you the UI behavior you’re looking forCasey Brooks
11/16/2022, 3:38 AM.sample
will capture the state and run the query as the user types, if they keep typing longer than the sampling period. Otherwise, a simple delay in .mapLatest
should also give a similar “buffering” effect, but only emit once the user has completely stopped typing
test.mapLatest {
delay(2.seconds)
queryFromDb()
}
Chris Fillmore
11/16/2022, 4:19 AMdebounce
is actually what he’s looking for. He wants to wait until the user is done typing before querying the db. Whereas sample
will query while the user is still typingChris Fillmore
11/16/2022, 4:24 AMdrop(1)
so that the initial empty string does not get sent as a queryFrancesc
11/16/2022, 6:02 AMfilterNot { it.isEmpty() }
Colton Idle
11/16/2022, 1:49 PM*latest
since if theres a long query underway and the user continues to type and a new value comes in, I want to effectively throw away the old value.Chris Fillmore
11/16/2022, 1:51 PMChris Fillmore
11/16/2022, 1:51 PMColton Idle
11/16/2022, 2:24 PMChris Fillmore
11/16/2022, 2:24 PMColton Idle
11/16/2022, 2:24 PMChris Fillmore
11/16/2022, 2:25 PMChris Fillmore
11/16/2022, 2:28 PMChris Fillmore
11/16/2022, 2:28 PMChris Fillmore
11/16/2022, 2:30 PMColton Idle
11/16/2022, 2:32 PMColton Idle
11/16/2022, 2:32 PMChris Fillmore
11/16/2022, 2:57 PMChris Fillmore
11/16/2022, 3:16 PMsample
appears to drop values in the trailing window. I was surprised to see 10 was not emittedChris Fillmore
11/16/2022, 3:18 PMChris Fillmore
11/16/2022, 3:24 PM...
* Note that the latest element is not emitted if it does not fit into the sampling window.
*/
@FlowPreview
public fun <T> Flow<T>.sample(periodMillis: Long): Flow<T> {
Ale Stamato
11/22/2022, 6:45 PMCasey Brooks
11/22/2022, 7:17 PMonValueChange
. viewModelScope.launch {}
by default runs on Dispatchers.Main.immediate
, so if you use a StateFlow
and don’t do anything else in viewModelScope.launch {}
, everything will work fine. StateFlows
themselves aren’t bound to any particular CoroutineDispatcher, so if it’s collected on Dispatchers.Main
in Compose (which it is, by default) and you update it on Dispatchers.Main
in the ViewModel (which it does, by default), then you shouldn’t have the kind of synchronization issue the article is talking about. You’d have the exact same problem with text synchronization if you used the same buggy version of viewModel.updateUsername()
with val _username: MutableState<String>
instead of val _username: MutableStateFlow<String>
. The article makes it seem like the StateFlow
is the problem, but it’s the async call to userRepository.isUsernameAvailable
that’s the real problem. If you set the value in the ViewModel immediately, and then launch a “fire-and-forget” in response to the changed text, you will be fineAle Stamato
11/22/2022, 7:22 PMIf you would still rather use StateFlow to store state, make sure you're collecting from the flow using the immediate dispatcher as opposed to the default dispatcher.
i just needed to flag it, in case you forget to do this and run into these issues.
also the intention of the article was not to be misleading, but to really really really recommend you away from stateflow/async reactive streams when working with textfieldCasey Brooks
11/22/2022, 7:50 PMvar username by mutableStateOf("")
val userNameHasError: StateFlow<Boolean> =
snapshotFlow { username }
.mapLatest { signUpRepository.isUsernameAvailable(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
fun updateUsername(input: String) {
username = input
}
is the “right” way to manage text, when this is just as correct, and is easier to write and understand
private val _username = MutableStateFlow("")
private val _userNameHasError = MutableStateFlow(false)
fun updateUsername(input: String) {
viewModelScope.launch {
_username.value = input
val isUsernameAvailable = userRepository.isUsernameAvailable(input)
_userNameHasError = !isUsernameAvailable
}
}
Casey Brooks
11/22/2022, 7:51 PMuserRepository.isUsernameAvailable
into a debounced query like the OP posted just fine, it won’t become “more correct” to back OPs text
with a State
instead of StateFlow
Francesc
11/22/2022, 8:08 PMimmediate
dispatcher so that the update to the username, in the textfield, is synchronous so it should work, but at the same time it depends on an implementation detail of the viewmodelScope
Colton Idle
11/23/2022, 3:52 PM