What are the best practices regarding two composit...
# compose
m
What are the best practices regarding two composition locals (representing global state) where one depends on the other. e.g one is a subset of the other? See code in 🧵
Suppose there is a composition local defined like:
Copy code
val LocalFooBarSnapshot = staticCompositionLocalOf<FooBarSnapshot> {
	...
}

data class FooBarSnapshot(
	val foo1: Foo1
	val foo2: Foo2
	val bar1: Bar1
	val bar2: Bar2
)
Now suppose we also have:
Copy code
data class FooSnapshot(
	val foo1: Foo1
	val foo2: Foo2
)
where:
Copy code
data class FooBarSnapshot(
	...
) {
	val foo by lazy {
		FooSnapshot(foo1, foo2)
	}
}
Is it okay to define:
Copy code
val LocalFooSnapshot = staticCompositionLocalOf<FooSnapshot> {
	...
}

@Composable
fun Theme() {
	...
            LocalFooBarSnapshot provides fooBarSnapshot,
            LocalFooSnapshot provides fooBarSnapshot.foo,
	...
	
}
My concern is that you can potentially end up with two different `FooSnapshot`s: one from
LocalFooBarSnapshot.current.foo
and another from
LocalFooSnapshot.current
(considering that any composable can provide its own
LocalFooSnapshot
to its child composables)
w
Assuming your field access mirrors your real code, I think you can use
providesComputed
.
m
In my case, even though
FooSnapshot
can be computed from
FooBarSnapshot
I’d rather not do so because
FooBarSnapshot
is expensive to calculate. They are both a subset of another snapshot. It’s just that
FooBarSnapshot
is a super set of
FooSnapshot
but with some additional expensive-to-compute properties.
But regardless of how they are computed, my main concern is having both
LocalFooBarSnapshot
and
LocalFooSnapshot
available to a composable.
w
I don't quite understand. The idea with computed is you can have some top level expensive object which is essentially cast to the individual composition locals.
m
But then the cheap objects are not available until the expensive object has been calculated. But still, this is not really what I’m concerned about. Perhaps it’s more of a generally data model problem, because we’re talking about redundant (potentially inconsistent) state being made available.
z
If your question is ā€œis having multiple sources of truth bad?ā€ the answer is generally yes, but the way you asked the question implies you know this, so I’m not sure what you want to hear šŸ˜…
šŸ˜… 1
m
Haha, right that wasn’t my question, but if you thought it was then I can see the confusion. My question is how to avoid multiple sources of truth in the case of composition locals. What is unique here is that composition locals can be overridden in a way that you don’t have with regular functions. Happy to try to explain better, if this is still not clear šŸ˜€.
Just don’t have composition locals with common state, I hear you say? But there are mechanisms in compose that explicitly allow for this. Furthermore, there are practical reasons why this might be desirable. So the question is, how to mitigate the drawbacks of multiple sources of truth here? Best practices etc. Has anyone thought about this I wonder.
z
It sounds like you don’t want to use
providesComputed
but I don’t understand why. You’re already computing both objects to provide them in your theme.
w
Maybe code helps? I don't have this in front of me, so this isn't exact, but the equivalent of your code would be
Copy code
fun test() {
    CompositionLocalProvider(
        LocalFooBar provides fooBarSnapshot,
        LocalFoo providesComputed { LocalFooBar.currentValue.foo },
    )
}
You can make
.foo
lazy if it's expensive and it'll only get computed the first time it's accessed.
m
If foo is expensive to calculate then it shouldn’t be executed in providesComputed lambda, regardless whether using lazy. Better to return some DeferredFoo instead. As mentioned earlier, FooBar is expensive to calculate and I want Foo to be available without waiting. So whilst it is possible to calculate Foo from FooBar, it is better to calculate it from an underlying state (from which both can be calculated). If providesComputed is used, can a child composable override it? If so, then there is a problem: LocalFooBar.foo1 can easily become misaligned with LocalFoo.foo1. Any composable that overrides LocalFoo would also have to override LocalFooBar. Another aspect is that these states need to be made available outside of compose which opens up another potential for state inconsistency.