Steffen Funke
11/02/2021, 4:04 PMderivedStateOf
be the State-equivalent of combine
in Flow / Rx-World? Meaning, if one of the inputs change, the resulting state should be recalculated? And can this also safely be used inside a ViewModel?
Background: I have e.g. a list and a search bar, and using MutableStateFlows
inside my ViewModel for data streams. Whenever the search term changes, I combine
the search term with the list, to filter it.
Works well and solid, but I think about going full Compose also in my ViewModels, therefore replacing my MutableStateFlow
s with Compose States.
Is this the correct approach, using derivedStateOf
?Archie
11/02/2021, 6:16 PMAdam Powell
11/02/2021, 7:21 PMWouldYes and no.be the State-equivalent ofderivedStateOf
in Flow / Rx-World?combine
derivedStateOf
caches the result of a computation based on State accessed during that computation. However, you don't need derivedStateOf
to combine several different state inputs.Adam Powell
11/02/2021, 7:23 PMclass Greeter {
var name by mutableStateOf("World")
var greeting by mutableStateOf("Hello")
val fullGreeting: String
get() = "$greeting $name"
}
with no derivedStateOf
at all; if you do
Text(greeter.fullGreeting)
then that will recompose any time either name
or greeting
change.Adam Powell
11/02/2021, 7:25 PMderivedStateOf
are lazy evaluation and caching if the inputs have not changed. This caching isn't free; if all you're doing is something like the above, the computation isn't particularly heavy, or you don't have many reads of fullGreeting
without inputs changing it's better to keep it simple and just compute on the fly.Steffen Funke
11/03/2021, 7:07 AMState<T>
directly. How does it know that / how is recomposition being triggered? 🤔
The version with derivedStateOf
works as well, but now looks like overkill. Have yet to come across a usecase for it - probably kind of for what `lazy var`s are used in plain Kotlin?
Thank you @Adam Powell for this enlightenment 👋Steffen Funke
11/03/2021, 7:09 AMderivedStateOf
, for sake of completeness:
//----------------------------------
// Repository
//----------------------------------
class SimpleSearchDemoRepo {
suspend fun getItemsFromApi(): List<String> = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
delay(1000)
val charPool = ('a'..'z') + ('A'..'Z') + ('0'..'9')
val items = (0..100).map {
(1..10).map {
charPool.random()
}.joinToString("")
}
items
}
}
//----------------------------------
// ViewModel
//----------------------------------
class SimpleSearchDemoViewModel(
viewModelScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
val repo: SimpleSearchDemoRepo
) {
init {
viewModelScope.launch { items = repo.getItemsFromApi() }
}
private var items: List<String> by mutableStateOf(emptyList())
var searchTerm: String by mutableStateOf("")
// Property getter
val filteredItems_propertyGetter: List<String>
get() = items.filter {
it.trim().lowercase().contains(searchTerm)
}
// derivedStateOf
val filteredItems_derivedState by derivedStateOf {
items.filter { it.trim().lowercase().contains(searchTerm) }
}
}
//----------------------------------
// UI
//----------------------------------
@ExperimentalMaterialApi
@Composable
fun SimpleSearchDemo() {
val viewModel by remember { mutableStateOf(SimpleSearchDemoViewModel(repo = SimpleSearchDemoRepo())) }
Column {
TextField(
value = viewModel.searchTerm,
onValueChange = {
viewModel.searchTerm = it
},
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
IconButton(onClick = { viewModel.searchTerm = "" }) {
Icon(imageVector = Icons.Default.Close, "")
}
}
)
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
items(viewModel.filteredItems_propertyGetter) { item ->
ListItem {
Text(item)
}
}
}
}
}
Steffen Funke
11/03/2021, 10:49 AMderivedStateOf
inside a ViewModel val
, what would be the best strategy to let the calculation run inside a coroutine?
At the moment I am playing around with a quite an expensive filter logic. With flows, I was able to move that calculation to a background dispatcher with flowOn
. I could still work around it by keeping this logic as a Flow
, and then populating my `mutableStateOf`s from it - but I wonder if there is a better way.Adam Powell
11/03/2021, 1:25 PMsnapshotFlow
to take a block of code that reads snapshot state and emits from a flow when things change, then do your usual redirection to other dispatchers if you like. At the end of a flow operator chain you can write another snapshot state value as part of your collect. This doesn't come up often though, since snapshots are generally intended to be atomic. Offloading derived snapshot state calculations to another coroutine dispatcher means you're probably giving up that atomicity unless you're doing something pretty sophisticated by hand with the low level snapshot APIsAdam Powell
11/03/2021, 1:28 PMAdam Powell
11/03/2021, 1:28 PMSteffen Funke
11/03/2021, 2:04 PMSteffen Funke
11/03/2021, 2:05 PMAdam Powell
11/03/2021, 2:32 PM