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

Alexander Maryanovsky

02/19/2022, 2:29 PM
There seems to be a general problem with Compose in that a lot of times it exposes state as a regular type, and its `State`fulness is implicit. For example, compose-desktop has
WindowState.position
of type
WindowPosition
and
WindowStateImpl
implements it as
Copy code
override var position by mutableStateOf(position)
but from just looking at the
WindowState
interface, there’s no way for you to know that
position
can be observed. Wouldn’t it better if the type of
WindowState.position
was
MutableState<WindowPosition>
?
🚫 2
a

Adam Powell

02/19/2022, 2:32 PM
No, in fact I still need to update the API guidelines doc to discourage both parameters of public functions and public properties of type
[Mutable]State<T>
Snapshot invalidation is about what was read or written, not about a container type exposed
The state types are implementation details, not required API surface, and hiding them from the API surface means a single property can access several snapshot state records and callers can invalidate accordingly when any of those records change. The caller doesn't have to be aware of the details.
👍 1
a

Alexander Maryanovsky

02/19/2022, 2:47 PM
Ok, but the fact that a value is observable is not an implementation detail. If
WindowState.position
was implemented as a regular property, things would break.
Maybe at least mark the property with an annotation, and have the compose plugin enforce its implementation.
a

Adam Powell

02/19/2022, 3:27 PM
That it snapshot invalidates is an API contract, as for annotations, one exists already:
@Stable
or perhaps more precisely, snapshots are runtime-only and based in API contracts. Lower level than
@Stable
or the compose-compiler plugin. Composition cares about
@Stable
, and if any stable type can visibly change, it must invalidate via the snapshot mechanism when it does.
a

Alexander Maryanovsky

02/19/2022, 3:35 PM
I'm not sure we're understanding each other. Do you see what problem I'm talking about? Do you think it's a valid concern?
a

Adam Powell

02/19/2022, 3:38 PM
Yes, I see the problem you're talking about. I think it's a concern born of familiarity with Flow and Rx, which express observability through stream types. It's a different system and it's easy to carry over assumptions that will hold back your designs in the long run.
I do think it's valid for public API types like
WindowState
to call out in their documentation if their properties are snapshot-observable, but with compose it's more of a baseline assumption.
but in terms of static analysis I don't think building such a system is desirable, for the same reasons checked exceptions have been dropped by many languages since java. Snapshots invalidate even through agnostic infrastructure code.
and this is a feature
the exception idea I'm getting at is elaborated on here: https://www.artima.com/articles/the-trouble-with-checked-exceptions
In the small, checked exceptions are very enticing. With a little example, you can show that you've actually checked that you caught the 
FileNotFoundException
, and isn't that great? Well, that's fine when you're just calling one API. The trouble begins when you start building big systems where you're talking to four or five different subsystems. Each subsystem throws four to ten exceptions. Now, each time you walk up the ladder of aggregation, you have this exponential hierarchy below you of exceptions you have to deal with. You end up having to declare 40 exceptions that you might throw. And once you aggregate that with another subsystem you've got 80 exceptions in your throws clause. It just balloons out of control.
a

Alexander Maryanovsky

02/19/2022, 3:44 PM
That's very much possible; I may develop different intuition for Compose over time. But where I’m standing now, I think trying to express as much of the behavior of an API as possible at compile-time is something to strive for. Don't you agree? I mean that's why I like static typed languages in the first place. Checked exceptions are a bit of an outlier. They're great in theory, but they're just too cumbersome in practice.
a

Adam Powell

02/19/2022, 3:45 PM
They're great in theory, but they're just too cumbersome in practice.
Yes, exactly. Snapshots assert that the same is true of explicit state invalidation.
Yes, you can become proficient at multiplexing reactive streams by knowing exactly the right combination and aggregation operators, and do so every time two objects meet or are dependencies of one another
a

Alexander Maryanovsky

02/19/2022, 3:47 PM
I'm all for the convenience of automatic subscription/invalidation. I just want a way to know whether a property is something I can (auto) subscribe to.
Are there no solutions to this that aren't cumbersome?
a

Adam Powell

02/19/2022, 3:48 PM
assume that anything that can change out from under you will snapshot invalidate
a

Alexander Maryanovsky

02/19/2022, 3:50 PM
Is that a good assumption for all Compose APIs?
a

Adam Powell

02/19/2022, 3:50 PM
yes
and in my experience it's a nice enough assumption to work with that it ends up naturally spreading 🙂
a

Alexander Maryanovsky

02/19/2022, 3:51 PM
Ok, I guess that answers most of my concern.
a

Adam Powell

02/19/2022, 3:52 PM
it does feel super weird at first
it freaked out some members of the android architecture components team quite a bit in the beginning 🙂
a

Alexander Maryanovsky

02/19/2022, 3:53 PM
I will come back here and tag you if I find any non-State properties in the API 😉
Is the overhead of State over a regular property that small that we can afford everything related to UI be State?
a

Adam Powell

02/19/2022, 4:01 PM
I think you'll find that just about anything in the compose API is either immutable or invalidates as state; other things are exceptions that prove the rule because they have some pretty special reasons not to be snapshot state 🙂
There isn't much in compose-runtime other than compositions themselves that have been optimized more aggressively than snapshot state. There's a little bit of thread atomic reads involved, a couple of indirections to reach the correct record if there are multiple snapshots currently open, and there's boxing of primitive types if you use
mutableStateOf<Float>
and similar.
that overhead isn't nothing, but generally you can play with the granularity of it. The alternative in a system where you need and are going to use that kind of observability is that you'd be creating immutable copies of objects to push through flows all the time, and you're really no worse off.
by play with the granularity I mean if you measure an issue with a big object with lots of individual properties, but they all usually change together, you might change the private implementation of that to be one
private var state by mutableStateOf(MyInternalState(a, b, c...))
and individual public properties might do something like
Copy code
val a: A
  get() = state.a
this is something specifically afforded by hiding which part of the implementation is the actual snapshot state record from the public API surface - changing from individual properties each being backed by their own snapshot state holder to batching or delegation from elsewhere is transparent to existing client code
and abstraction-wise it means that you can define some properties declaratively in terms of properties and operations on other objects
you might have some ViewModel-like object with a property defined like this:
Copy code
val isLoggedIn: Boolean
  get() = myRepository.user != null && authDelegate.token.isValid
or anything of the sort
as these expressions of state get more complex it becomes a lot clearer and easier to express them in plain kotlin than as stream combinations and transformations, especially through different layers of abstraction that may or may not have any domain knowledge at all
a

Alexander Maryanovsky

02/19/2022, 4:19 PM
I think I’m starting to understand. The user code can be unaware of which of the values along the data chain/tree are immutable and which are observable.
You do have to recompile though
a

Adam Powell

02/19/2022, 4:20 PM
only the classes that changed
a

Alexander Maryanovsky

02/19/2022, 4:20 PM
So it's not ideal
a

Adam Powell

02/19/2022, 4:20 PM
there's no compiler magic involved with snapshots
you can use them without building with the compose-compiler plugin enabled at all
a

Alexander Maryanovsky

02/19/2022, 4:22 PM
But the code that causes recomposition is compiler magic, no?
a

Adam Powell

02/19/2022, 4:22 PM
the answer to that specific question is subtle in a few ways that I don't think matter in this case 🙂
composition will observe any snapshot changes that happen during recomposition
a new one appearing somewhere does not change the code that the composition process would run to perform that observation
to make it concrete, the API composition uses for this is
Snapshot.takeMutableSnapshot
and then it performs composition with that snapshot active
you'll note that it accepts optional observer callbacks when you take a snapshot, those are invoked any time a snapshot record is read or written inside the snapshot
composition just adds anything reported that way to a set of things to watch for later, it doesn't care what they are or how many there are
so no, you do not need to recompile your composable functions when you make some other class use snapshot state
a

Alexander Maryanovsky

02/19/2022, 4:26 PM
If I access
window.state.position.x
and when I compile
state
is a State and
position
is immutable and then the library changes and
state
is immutable and
position
is a State, I need to recompile, no?
a

Adam Powell

02/19/2022, 4:27 PM
modulo the behavior of your specific build system and how it performs linking when you update an implementation dependency, no, you don't.
window.state.position.x
is a call to a kotlin property getter method; that doesn't change.
you've only changed the implementation of that property getter
it's the same as if you changed the implementation from
Copy code
var x: Int
  private set
to
Copy code
val x: Int
  get() = position.x

private var position = Offset(0, 0)
that's a binary compatible change
a

Alexander Maryanovsky

02/19/2022, 4:32 PM
I'm confused. All the tutorials say that the way recomposition is triggered is that when you access State.value, the compiler writes code that subscribes the composition to changes in that State. If something used to be a regular value and is now State.value, how can it work without recompilation? The subscription happens at the call site, no?
a

Adam Powell

02/19/2022, 4:35 PM
then those tutorials are giving bad information and if they're coming from google we should reword them to be accurate. 🙂
no composition involved here at all
if you make a plain kotlin command line app, add an implementation dependency on
compose-runtime
, and do not include anything related to the
compose-compiler
plugin, you can still use
snapshotFlow
to listen for changes and
Snapshot.withMutableSnapshot {}
to publish changes
this is the implementation of
MutableState.value
's getter
note how it notifies the snapshot's read observer whenever the snapshot state is accessed
a

Alexander Maryanovsky

02/19/2022, 4:43 PM
Thanks, I have a lot to read and understand
👍 1
3 Views