https://kotlinlang.org logo
#compose
Title
# compose
g

Grigorii Yurkov

11/02/2020, 2:34 PM
What is
map
analog for
MutableState
?
a

Adam Powell

11/02/2020, 2:42 PM
Map as in a data structure or map as in a functional map operation?
g

Grigorii Yurkov

11/02/2020, 2:42 PM
functional map operation
a

Adam Powell

11/02/2020, 2:43 PM
Any function that performs a computation using a mutableStateOf input will work, no need to create a pipeline of wrappers
And by, "work" I mean it will be observed as a read by the snapshot system
g

Grigorii Yurkov

11/02/2020, 2:47 PM
Copy code
class MyViewModel : ViewModel() {
var string by mutableState("1")
val int get() = string.toInt()
}
Like this?
I think this wont work
a

Adam Powell

11/02/2020, 2:48 PM
Yep, any reads of
string
are tracked, and the getter reads
string
Oh, not with
mutableLiveData
though 🙂
g

Grigorii Yurkov

11/02/2020, 2:49 PM
Really? I thought it works only in composable functions
It's typo
👍 1
a

Adam Powell

11/02/2020, 2:50 PM
It's part of a lower level snapshot tracking system than composable functions. If you want to do your own observation of snapshot state in a similar way, see
snapshotFlow {}
We use similar tracking in compose for layout and drawing, which aren't composable functions but we use snapshot state changes as scoped invalidation signals for those UI phases
g

Grigorii Yurkov

11/02/2020, 2:59 PM
I am really surprised that this works with no composable functions. I don't understand how is it even possible.
it's a transaction system. When you open a snapshot it's thread-local, and you can either commit or abort that snapshot
when a snapshot is committed it is made visible to other threads and future snapshots can build on it
during a transaction - when a snapshot is open - you can track reads and writes. You can also globally observe points when new transactions are committed
it's that last part that compose uses to trigger invalidations
when composable functions (or layout or drawing code) run, we set up a local snapshot observation to see what state objects are read in that scope
and it doesn't care how many layers of function calls it goes through
later on when snapshots are applied, we check to see if any of the objects we care about change for each invalidation scope and if so, we invalidate the relevant components
finally, there's an idea of a, "global transaction" - if you haven't explicitly opened a transaction you can still freely read/write snapshot state objects
and some platform-specific code sets up a periodic event to send apply notifications to observers if any global writes like that occurred
g

Grigorii Yurkov

11/02/2020, 3:32 PM
This is super interesting, im going to deep dive into it
👍 1
a

Adam Powell

11/02/2020, 3:38 PM
the other neat thing it gets you is global consistency for thread safety. When you take a snapshot and work within it, even if it changes on another thread you're always working with consistent state from the point when you took that snapshot
If you write
Copy code
var one = mutableStateOf("one")
var two = mutableStateOf("two")
val myFlow = snapshotFlow { "$one and $two" }
then you can take advantage of transactional changes of both
one
and
two
- they can't get out of sync with one another
g

Grigorii Yurkov

11/02/2020, 5:17 PM
@Adam Powell I want to officially declare - you are freaking wizards. I created a bridge property between state property and composable functions using reflection.
Copy code
class StateHolder {
    var state by mutableStateOf(0)

    var stateBridgeProperty: Int
        get() {
            val clazz = Class.forName("ru.rpuxa.composeexperiments.StateHolder")
            val method = clazz.getMethod("getState")
            val value = method.invoke(this) as Int
            return value
        }
        set(value) {
            val clazz = Class.forName("ru.rpuxa.composeexperiments.StateHolder")
            val method = clazz.declaredMethods.find { it.name == "setState" }!!
            method.invoke(this, value)
        }
}
Then I created two composable functions that just count number of clicks
Copy code
val first = StateHolder()
val second = StateHolder()

@Composable
fun MyTest() {
    Log.d("MyDebug", "First function recomposed")
    Column {
        Text(text = first.stateBridgeProperty.toString())
        Button(onClick = {
            first.stateBridgeProperty++
        }) {
            Text("++")
        }
    }
}

@Composable
fun MyTest2() {
    Log.d("MyDebug", "Second function recomposed")
    Column {
        Text(text = second.stateBridgeProperty.toString())
        Button(onClick = {
            second.stateBridgeProperty++
        }) {
            Text("++")
        }
    }
}
I have a question - when I click the first button HOW do you understand you need to recompose first function and don't touch second? I understand you see that
first.state
changed but you must have no idea that
first.stateBridgeProperty
connected with
first.state
and therefore you must not know you need to recompose first function.
a

Adam Powell

11/02/2020, 5:29 PM
it's the methods on the
SnapshotMutableState
object that perform the tracking. In the case of your reflection example above, those methods still get called so the tracking still happens: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/MutableState.kt;l=293?q=SnapshotMutableState
g

Grigorii Yurkov

11/02/2020, 5:32 PM
Hm, I am starting to understand something
I need more experiments 🙂
🔬 1
it shows how reads of state values are tracked
o

Oleg Khotskin

11/02/2020, 8:43 PM
Hi! As I understand, derivedStateOf is what you are looking for.
a

Adam Powell

11/02/2020, 8:52 PM
Not necessarily. derivedStateOf creates an additional caching object that interacts a bit differently with the snapshot system. Usually you don't need it vs the simpler approach of just computing based on state inputs when needed