How Compose Present Views
About 3 min
How Compose Present Views
Introduction
In Android Studio formal tutorial, it uses the example below to show a compose view.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeLearningProjectTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
We can see it uses setContent to hold a Theme which contains actual compose view. So what exactly does the setContent
block do?
Tree
ComposeView, Actually a ViewGroup
ComponentActivity
setContent
Core: Actually set a ComposeView
in traiditional way. Like delegating all the steps to a single ComposeView
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
// try to find an existing compose view (normally null)
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}
- Try to find an existing compose view (normally null)
- If it has an exisiting compose view, then...
- If it doesn’t, it actually set a
ComposeView
to the layout by following steps- Build a ComposeView
- Set parent composition context for compose view
- Set Content
- Set owners: Compatiate with old version, set lifecycle/viewModelStoreOwner/SavedStateRegistryOwner
- Set ContentView for layout
What is a ComposeView?
First, we’d better take a look at AbstractComposeView
, which is the basic for Jetpack Compose.
AbstractComposeView
abstract class AbstractComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
init {
clipChildren = false
clipToPadding = false
}
private var parentContext: CompositionContext? = null
private var cachedViewTreeCompositionContext: WeakReference<CompositionContext>? = null
@Composable
@UiComposable
abstract fun Content()
}
ComposeView
class ComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
private val content = mutableStateOf<(@Composable () -> Unit)?>(null)
@Suppress("RedundantVisibilityModifier")
protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
@Composable
override fun Content() {
content.value?.invoke()
}
override fun getAccessibilityClassName(): CharSequence {
return javaClass.name
}
/**
* Set the Jetpack Compose UI content for this view.
* Initial composition will occur when the view becomes attached to a window or when
* [createComposition] is called, whichever comes first.
*/
fun setContent(content: @Composable () -> Unit) {
shouldCreateCompositionOnAttachedToWindow = true
this.content.value = content
if (isAttachedToWindow) {
createComposition()
}
}
}
Runing
onAttachToWindow
First, onAttachToWindow
will be touched because ViewqRootImpl
do performTraversals
override fun onAttachedToWindow() {
// ViewGroup
super.onAttachedToWindow()
previousAttachedWindowToken = windowToken
// Must be true, because already set in `setContent`
if (shouldCreateCompositionOnAttachedToWindow) {
ensureCompositionCreated()
}
}
private fun ensureCompositionCreated() {
if (composition == null) {
try {
creatingComposition = true
composition = setContent(resolveParentCompositionContext()) {
Content()
}
} finally {
creatingComposition = false
}
}
}
private fun resolveParentCompositionContext() = parentContext
?: findViewTreeCompositionContext()?.cacheIfAlive() // null in init
?: cachedViewTreeCompositionContext?.get()?.takeIf { it.isAlive } // null in init
?: windowRecomposer.cacheIfAlive()
/**
* Get or lazily create a [Recomposer] for this view's window. The view must be attached
* to a window with the [LifecycleOwner] returned by [findViewTreeLifecycleOwner] registered at
* the root to access this property.
*/
@OptIn(InternalComposeUiApi::class)
internal val View.windowRecomposer: Recomposer
get() {
check(isAttachedToWindow) {
"Cannot locate windowRecomposer; View $this is not attached to a window"
}
val rootView = contentChild
// null
return when (val rootParentRef = rootView.compositionContext) {
null -> WindowRecomposerPolicy.createAndInstallWindowRecomposer(rootView)
is Recomposer -> rootParentRef
else -> error("root viewTreeParentCompositionContext is not a Recomposer")
}
}
private val View.contentChild: View
get() {
var self: View = this
var parent: ViewParent? = self.parent
while (parent is View) {
if (parent.id == android.R.id.content) return self
self = parent
parent = self.parent
}
return self
}
SetContent
Build AndroidCOmposeView
actually, and add it to ComposeView
internal fun AbstractComposeView.setContent(
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
val composeView =
if (childCount > 0) {
getChildAt(0) as? AndroidComposeView
} else {
removeAllViews(); null
} ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
return doSetContent(composeView, parent, content)
}
private fun doSetContent(
owner: AndroidComposeView,
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
if (inspectionWanted(owner)) {
owner.setTag(
R.id.inspection_slot_table_set,
Collections.newSetFromMap(WeakHashMap<CompositionData, Boolean>())
)
enableDebugInspectorInfo()
}
val original = Composition(UiApplier(owner.root), parent)
val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
as? WrappedComposition
?: WrappedComposition(owner, original).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
wrapped.setContent(content)
return wrapped
}
Composition
Composition
interface Composition {
/**
* Returns true if any pending invalidations have been scheduled. An invalidation is schedule
* if [RecomposeScope.invalidate] has been called on any composition scopes create for the
* composition.
*
* Modifying [MutableState.value] of a value produced by [mutableStateOf] will
* automatically call [RecomposeScope.invalidate] for any scope that read [State.value] of
* the mutable state instance during composition.
*
* @see RecomposeScope
* @see mutableStateOf
*/
val hasInvalidations: Boolean
val isDisposed: Boolean
/**
* Clear the hierarchy that was created from the composition and release resources allocated
* for composition. After calling [dispose] the composition will no longer be recomposed and
* calling [setContent] will throw an [IllegalStateException]. Calling [dispose] is
* idempotent, all calls after the first are a no-op.
*/
fun dispose()
/**
* Update the composition with the content described by the [content] composable. After this
* has been called the changes to produce the initial composition has been calculated and
* applied to the composition.
*
* Will throw an [IllegalStateException] if the composition has been disposed.
*
* @param content A composable function that describes the content of the composition.
* @exception IllegalStateException thrown in the composition has been [dispose]d.
*/
fun setContent(content: @Composable () -> Unit)
}
ControlledComposition
/**
* A controlled composition is a [Composition] that can be directly controlled by the caller.
*
* This is the interface used by the [Recomposer] to control how and when a composition is
* invalidated and subsequently recomposed.
*
* Normally a composition is controlled by the [Recomposer] but it is often more efficient for
* tests to take direct control over a composition by calling [ControlledComposition] instead of
* [Composition].
*
* @see ControlledComposition
*/
sealed interface ControlledComposition : Composition {
val isComposing: Boolean
val hasPendingChanges: Boolean
}
CompositionImpl
internal class CompositionImpl(
/**
* The parent composition from [rememberCompositionContext], for sub-compositions, or the an
* instance of [Recomposer] for root compositions.
*/
private val parent: CompositionContext,
/**
* The applier to use to update the tree managed by the composition.
*/
private val applier: Applier<*>,
recomposeContext: CoroutineContext? = null
) : ControlledComposition {
private val pendingModifications = AtomicReference<Any?>(null)
// Held when making changes to self or composer
private val lock = Any()
}
WrappedComposition
private class WrappedComposition(
val owner: AndroidComposeView,
val original: Composition
) : Composition, LifecycleEventObserver {
}