Would `derivedStateOf` be the State-equivalent of ...
# compose
s
Would
derivedStateOf
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
?
👌 2
a
This is exactly how I think about it as well.
🙏 1
a
Would 
derivedStateOf
 be the State-equivalent of 
combine
 in Flow / Rx-World?
Yes and no.
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.
It's just as valid to do this:
Copy code
class 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
Copy code
Text(greeter.fullGreeting)
then that will recompose any time either
name
or
greeting
change.
🙏 3
The key advantages of
derivedStateOf
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.
❤️ 1
s
Wow, to be able to use plain old property getters and achieve this behavior just blew my mind. Kind of magical to me, that compose detects those changes as well, even while bound to a property getter and not to a
State<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 👋
For anyone interested, this is my working sample code of list filtering with a) property getter, and b)
derivedStateOf
, for sake of completeness:
Copy code
//----------------------------------
//  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)
                }
            }
        }
    }
}
In that regard, when using
derivedStateOf
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.
a
Once you start wanting to compute derived things asynchronously then the tools get more complicated as well. You can use
snapshotFlow
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 APIs
As for how it works, the current snapshot is a matter of some custom ThreadLocal tracking, so it doesn't matter where in the call stack you are when a snapshot read or write is performed, it's tracked.
The compose compiler plugin isn't involved with snapshots at all
s
Makes sense thanks. Need to wrap my head more around the Compose Snapshot system.
Really admire your patience to answer all those questions 🙂 👍
👍 1
a
no problem 🙂 it helps me understand where the community is at and gives me something to do while I'm drinking my morning coffee 😄
😍 1
🙏 1
1