19.5 Compose CompositionContext 和 再談 CompositionLocal
CompositionContext
終於可以說說 CompositionContext 了,放在最後是因為這東西沒法上來直接說,需要我們前面的累積。我們來看下這個類的介紹
/**
* A [CompositionContext] is an opaque type that is used to logically "link" two compositions
* together. The [CompositionContext] instance represents a reference to the "parent" composition
* in a specific position of that composition's tree, and the instance can then be given to a new
* "child" composition. This reference ensures that invalidations and [CompositionLocal]s flow
* logically through the two compositions as if they were not separate.
*
* The "parent" of a root composition is a [Recomposer].
*
* @see rememberCompositionContext
*/
@OptIn(InternalComposeApi::class)
abstract class CompositionContext internal constructor()
總結一下
1. “子” CompositionContext 可以將兩個 Composition 邏輯上鍊接在一起,產生邏輯上的父子關係。
CompositionContext 儲存在 “父” Composition 的 SlotTable 中。
這個“子” CompositionContext 保證了兩個 Composition 使用同一個 CompositionLocalMap 和 invalidations 邏輯。
2. 根 Composition 的 parentContext 就是 Activity#setContent() 時生成的 Recomposer 。
子 Composition 的 parentContext 使用 rememberCompositionContext() 方法生成。
Composition 邏輯父子關係中 CompositionContext 的型別用來判斷 Composition 是不是最上層的“父” Composition。
internal class CompositionImpl(private val parent: CompositionContext){
val isRoot: Boolean = parent is Recomposer
}
CompositionContext 就兩個實現類
CompositionContext
|-- Recomposer 根
|-- ComposerImpl.CompositionContextImpl 子
Recomposer 前面的章節已經介紹過了,這裡我們來看 CompositionContextImpl
ComposerImpl.CompositionContextImpl
CompositionContextImpl 使用 rememberCompositionContext() 方法生成,這個方法在 Popup() 中有使用
``` @Composable fun Popup() { val parentComposition = rememberCompositionContext() val popupLayout = remember { PopupLayout().apply { setContent(parentComposition) { } } } }
internal class PopupLayout(){ fun setContent(parent: CompositionContext, content: @Composable () -> Unit) { setParentCompositionContext(parent) this.content = content shouldCreateCompositionOnAttachedToWindow = true } } ``` 由原始碼可見 PopupLayout 的 parentContext 是 rememberCompositionContext() 方法生成的,而不是複用 ComposeView 在初始組合流程中生成的 Recomposer。
``` @Composable fun rememberCompositionContext(): CompositionContext { return currentComposer.buildContext() }
internal class ComposerImpl( override fun buildContext(): CompositionContext { startGroup(referenceKey, reference) if (inserting) writer.markGroup()
var holder = nextSlot() as? CompositionContextHolder if (holder == null) { //建立 CompositionContextHolder 和 CompositionContextImpl 物件 //holder.ref 是 CompositionContextImpl 物件 holder = CompositionContextHolder( CompositionContextImpl( compoundKeyHash, forceRecomposeScopes ) ) //holder 儲存到“父” Composition 的 SlotTable 中 updateValue(holder) } //將父 Composition Slot中的 CompositionLocalMap 儲存到 //CompositionContextImpl.compositionLocalScope 中 holder.ref.updateCompositionLocalScope(currentCompositionLocalScope()) endGroup() //返回 CompositionContextImpl 物件 return holder.ref } } ``` 生成一個被 CompositionContextHolder 持有的 CompositionContextImpl 物件 ,
將 holder 儲存到“父” Composition 的 SlotTable 中
傳遞 CompositionLocalMap
返回 CompositionContextHolder 持有的 CompositionContextImpl 物件
隨後這個 CompositionContextImpl 作為 PopupLayout 的 parentContext 參與到 PopupLayout 的初始組合和重組中。
CompositionContextHolder 結構簡單,實現了 RememberObserver ,這是一個很重要的介面,我們下章來學習它。
private class CompositionContextHolder(
val ref: ComposerImpl.CompositionContextImpl
) : RememberObserver {
override fun onRemembered() { }
override fun onAbandoned() {
ref.dispose()
}
override fun onForgotten() {
ref.dispose()
}
}
CompositionContext 類註釋中說:
“子” CompositionContext 保證了兩個 Composition 使用同一個 CompositionLocalMap 和 invalidations 邏輯。
同一個 CompositionLocalMap,buildContext() 原始碼有體現 ,具體後面會分析。
holder.ref.updateCompositionLocalScope(currentCompositionLocalScope())
同一個 invalidations 邏輯,看 CompositionContextImpl 原始碼就明白了。
``` //原始碼不完整 private inner class CompositionContextImpl( override val compoundHashKey: Int, override val collectingParameterInformation: Boolean ) : CompositionContext() { override val effectCoroutineContext: CoroutineContext get() = parentContext.effectCoroutineContext
override fun composeInitial( composition: ControlledComposition, content: @Composable () -> Unit ) { parentContext.composeInitial(composition, content) }
override fun invalidate(composition: ControlledComposition) { //先處理父 composition 再處理自己 parentContext.invalidate([email protected]) parentContext.invalidate(composition) } } ``` “子” Composition 中的 invalidations 邏輯 最後都是由“根” Composition 中的 Recomposer 來發起的。
再談 CompositionLocal
14.1 的時候介紹過 CompositionLocal ,只說了 Android 預設新增的 CompositionLocal 和 如何自定義 CompositionLocal,說倒 CompositionLocalProviderI() 就結束了。
這裡我們再深入瞭解
- CompositionLocal 如何解析
- CompositionLocal 如何在“父子” Composition 中傳遞
CompositionLocal解析流程
setContent 中 @Composable content 外層會新增預設的 CompositionLocalMap ``` original.setContent {
CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
ProvideAndroidCompositionLocals(owner, content)
}
}
@Composable @OptIn(InternalComposeApi::class) fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) { currentComposer.startProviders(values) content() currentComposer.endProviders() } ``` CompositionLocalMap 的解析呼叫的不是 startGroup() ,而是 startProviders()
``` @InternalComposeApi override fun startProviders(values: Array<out ProvidedValue<*>>) { // 拿到當前 Composition 中的 CompositionLocalMap val parentScope = currentCompositionLocalScope() startGroup(providerKey, provider) startGroup(providerValuesKey, providerValues) // 將 values 解析成 CompositionLocalMap val currentProviders = invokeComposableForResult(this) { compositionLocalMapOf(values, parentScope) } endGroup() val providers: CompositionLocalMap val invalid: Boolean if (inserting) { //合併 parentScope currentProviders providers = updateProviderMapGroup(parentScope, currentProviders) invalid = false //標記當前插入操作的 wirter 已經有了 Provider writerHasAProvider = true } else { /不是新增,判斷 currentProviders 的和 SlotTable 中儲存的是否一樣 //一樣就忽略,不一樣就更新 }
if (invalid && !inserting) { providerUpdates[reader.currentGroup] = providers } providersInvalidStack.push(providersInvalid.asInt()) providersInvalid = invalid //合併後的 快取在 providerCache providerCache = providers // groupKey groupObjectKey 不是LayoutNode 合併後的 providers // 將合併後的 providers 儲存到 當前 Composition 的 SlotTable 中 start(compositionLocalMapKey, compositionLocalMap, false, providers) }
```
``` private fun currentCompositionLocalScope(group: Int? = null): CompositionLocalMap { if (group == null) //已經有快取了直接返回 providerCache?.let { return it } // 如果當前是插入操作且標記過 writerHasAProvider // 程式碼的 if(inserting) 邏輯 if (inserting && writerHasAProvider) { var current = writer.parent while (current > 0) { if (writer.groupKey(current) == compositionLocalMapKey && writer.groupObjectKey(current) == compositionLocalMap ) { @Suppress("UNCHECKED_CAST") val providers = writer.groupAux(current) as CompositionLocalMap //快取 providers 並返回 providerCache = providers return providers } current = writer.parent(current) } } //沒有進行寫操作,去 reader 裡找 ,找到就快取 providers 並返回 if (reader.size > 0) { var current = group ?: reader.parent while (current > 0) { if (reader.groupKey(current) == compositionLocalMapKey && reader.groupObjectKey(current) == compositionLocalMap ) { @Suppress("UNCHECKED_CAST") val providers = providerUpdates[current] ?: reader.groupAux(current) as CompositionLocalMap providerCache = providers return providers } current = reader.parent(current) } } //都沒有就快取 parentProvider 並返回 providerCache = parentProvider return parentProvider }
//預設 空 private var parentProvider: CompositionLocalMap = persistentHashMapOf() ``` 舉個例子:
CompositionLocal 傳遞過程
在 buildContext() 時把當前 Composition 中快取的 ComposerImpl.providerCache 賦值給了CompositionContextImpl.compositionLocalScope。 ``` // ComposerImpl.buildContext() holder.ref.updateCompositionLocalScope(currentCompositionLocalScope())
//CompositionContextImpl fun updateCompositionLocalScope(scope: CompositionLocalMap) { compositionLocalScope = scope } ``` CompositionContextImpl 作為 CompositionContext 作為引數建立 PopupLayout 的 Composition 和 Composer , PopupLayout 的初始組合開啟。
invokeComposable() 也就是解析 PopupLayout 的 @Composable content 之前會先呼叫 startRoot() 方法
private fun doCompose() {
trace("Compose:recompose") {
try {
startRoot() //CompositionLocal 在這裡傳遞
observeDerivedStateRecalculations() {
if (content != null) {
startGroup(invocationKey, invocation)
invokeComposable(this, content)
endGroup()
} else if () {
} else {}
}
endRoot()
} finally {}
}
}
此時的 parentContext 就是 buildContext() 返回的 CompositionContextImpl 物件。
@OptIn(InternalComposeApi::class)
private fun startRoot() {
//CompositionContextImpl.compositionLocalScope
//將 ComposeView 中 Composer 快取的 providers 賦值給
//當前 PopupLayout 中 Composer 的 parentProvider
parentProvider = parentContext.getCompositionLocalScope()
}
再執行 invokeComposable() 解析 PopupLayout 的 @Composable content 。
同樣會先執行 startProviders(),此時雖然 PopupLayout 剛剛開啟初始組合但第一次執行 currentCompositionLocalScope() 並不會返回空 Map ,而是傳遞過來的 providers。
``` //預設 空 //private var parentProvider: CompositionLocalMap = persistentHashMapOf() // ↑ private var parentProvider = parentContext.getCompositionLocalScope()
private fun currentCompositionLocalScope(group: Int? = null): CompositionLocalMap { providerCache = parentProvider return parentProvider } ``` 此時的 startProviders() 會以傳遞過來的 providers 為基礎生成新的 providers 儲存到 PopupLayout 的 Composition 的 SlotTable 中,同時快取到 Composer 中。
在上面的例子中新增 Popup() , CompositionLocal 傳遞如下圖