I don’t understand `compositionLocalWithComputedDe...
# compose
m
I don’t understand
compositionLocalWithComputedDefaultOf
. I’ve read the code a little, and it seems that
.provides(T): ProvidedValue<T>
returns one where
compute = null, isDynamic = true
which means
Copy code
val local = compositionLocalWithComputedDefaultOf {/* ... */}

CompositionLocalProvider(local provides x) {/* ... */}
The
CompositionLocalProvider
will remember a
DynamicValueHolder
for it at that position in the composition like it does for locals constructed by
compositionLocalOf
, which means they’re backed by mutable states and their readers are recomposed on invalidation. Once you’ve explicitly provided a value, there’s no difference between a local by
compositionLocalWithComputedDefaultOf
and
compositionLocalOf
. So when do I use it and why?
Copy code
val key = compositionLocalOf<String> {/* ... */}

CompositionLocalProvider(key provides "value") {
    val nested = compositionLocalWithComputedDefaultOf { key.currentValue }
}
Upon further investigation, I see that
nested
cannot be created by
compositionLocalOf
. How does
compositionLocalWithComputedDefaultOf
make that work though?
It seems that
someLocal.current
when a value hasn’t been provided calls 1.
currentComposer.consume(this)
where
this = someLocal
2.
this.currentCompositionLocalScope().read(key)
where
this = currentComposer
3.
this.getOrElse(key as CompositionLocal<Any?>) { key.defaultValueHolder }.readValue(this) as T
where
this
is the return value of
currentCompositionLocalScope
4.
map.compute()
in
readValue
where
map
is its parameter, and
this
is the return value of
getOrElse
which is a
ComputedValueHolder<T>(val compute: CompositionLocalAccessorScope.() -> T)
in the case of a
compositionLocalWithComputedDefaultOf
local Why did this have to be a separate
ProvidableCompositionLocal
subtype though? Why couldn’t this be enabled in
compositionLocalOf
?
s
Mostly because of backwards compat, we didn't have the accessor scope until 1.10 roughly
m
@shikasd Is there a reason the
defaultFactory
parameter type is
CompositionLocalAccessorScope.() -> T
in
compositionLocalWithComputedDefaultOf<T>
? Why couldn’t it have been
@Composable () -> T
?
Copy code
// If `defaultFactory: @Composable () -> T`
// In `compositionLocalWithComputedDefaultOf<T>`
val key = compositionLocalWithComputedDefaultOf { someCompLocal.current }
What problem does
CompositionLocalAccessorScope.() -> T
solve that
@Composable () -> T
would not have?
s
Composition locals can be read outside of composition (e.g. modifier nodes), and we don't want to involve composition in those cases
m
Composition locals can be read outside of composition
@shikasd Really? The docs don’t even allude to that fact, so that’s quite disconcerting, but I massively appreciate your clarification.
Copy code
public object LocalViewModelStoreOwner {
    private val LocalViewModelStoreOwner =
        compositionLocalWithHostDefaultOf(ViewModelStoreOwnerHostDefaultKey)
    public val current: ViewModelStoreOwner?
        @Composable get() = LocalViewModelStoreOwner.current
    public infix fun provides(
        viewModelStoreOwner: ViewModelStoreOwner
    ): ProvidedValue<ViewModelStoreOwner?> {
        return LocalViewModelStoreOwner.provides(viewModelStoreOwner)
    }
}
Could you also explain why it wraps a
ProvidableCompositionLocal<ViewModelStoreOwner?>
to manually reimplement its instance members please? A wrapper typically exists to intercept and mediate access, but this
object
relays everything as-is.
s
Yeah, check currentValueOf in Modifier.Node No idea what ViewModel is implemented this way, might something about supporting different platforms
m
@shikasd Yeah I’ve tested this out and briefly followed the call chain
Copy code
class MyModifier: Modifier.Node(), CompositionLocalConsumerModifierNode {
    companion object {
        val LocalKey = compositionLocalOf { 0 }
    }
    override fun onAttach() {
        Log.d("MY_MODIFIER",
            @SuppressLint("SuspiciousCompositionLocalModifierRead")
            this.currentValueOf(MyModifier.LocalKey).toString()
        )
    }
}
Also
LocalViewModelStoreOwner
is not a viewmodel. It’s an object in
androidx.lifecycle.viewmodel.compose
.
e
@min I already answered you here https://kotlinlang.slack.com/archives/CJLTWPH7S/p1784797026286239?thread_ts=1784715762.471929&amp;cid=CJLTWPH7S. The way KxCoroutines providing
asSharedFlow()
API to wrap a
MutableSharedFlow
so that the underlying object type is not mutable, is the same way the
ProvidableCompositionLocal
needs to be encapsulated so that its not mutable outside of the internal library.
m
@efemoney Prevent what mutations?
e
Change the value of the composition local.
m
@efemoney Can you show me an example that would have worked if it weren’t for this encapsulation that this encapsulation prevents?
c
To answer the original question, the example,
Copy code
val local = compositionLocalWithComputedDefaultOf {/* ... */}

CompositionLocalProvider(local provides x) {/* ... */}
In this example there is no difference between using
compositionLocalOf()
and
compositionLocalWithComputedDefaultOf()
Where the two diverges is what happens when you don't provide
local
, what is the
value
of
local
? If a
compositionLocalOf()
composition local is not provided
defaultValue
is invoked (the first time, it is cached after that) and is returned as
value
. In
compositionLocalWithComputedDefaultOf()
the compute lambda is called every time but it is also a `CompositionLocalAccessorScope`so it can read other composition locals to produce its value. An example of this use is, assume you have a holder of values (think
Context
in Android) but you want to surface parts of the holder as separate composition locals (such as default text direction). Before computed composition locals, you were required to provide each local separately. Now you can declare the individual holders as computed by default which will read the holder and return the value. Then providing a new holder all the individual locals will now report values from the new holder without requiring their being explicitly re-provided.
1
m
@Chuck Jazdzewski [G] Do you also know why
LocalViewModelStoreOwner
is a wrapper? It doesn’t prevent or enable anything. I wonder if this is vibecoded.
c
This was created to prevent tight coupling between Compose, ViewModel and Nav3.
m
@Chuck Jazdzewski [G] Right, could I please see an example of coupling that this abstraction prevents? One that would have worked if
LocalViewModelStoreOwner
was a bare
CompositionLocal<ViewModelStoreOwner?>