preface

With the rapid development of the big front-end concept, there are many languages and frameworks for declarative UI writing, such as the react front-end, iOS Swift UI, and Google Flutter, but you rarely hear of any revolutionary declarative UI technology or cross-platform support native to Android. Now, Google is coming up with Compose. Here, we’ll take a brief look at Compose and take a look at the mystery of KMM.

Compose: What is Compose?

The development route

  • Compose, the new UI library announced at Google I/O 2019, is a new addition to Jetpack
  • Compose finally got its 1.0 release in July 2021
  • Compose released version 1.1 in February 2022

Introduction to the

Official: Less code, intuitive, faster development, powerful declarative UI; Easier customization; Real-time, interactive preview functionality; More performance and functionality.

AndroidView versus Compose

AndroidView writing

Generally written in XML, most imperative UI, code to command, to command interface updates, but combined with DataBinding can achieve declarative, pay attention to the hierarchy when writing, view tree measure multiple measurement time

Why is yu born bright?

Compose writing

For example, Compose disables the Intrinsic Measurement function for user interface, which is composed to reflect the Intrinsic Measurement of the user interface. For example, Compose disables the Intrinsic Measurement function for user interface.

The Intrinsic Measurement that Compose allows the parent component to measure the “Intrinsic Measurement” of the component before measuring the child component.


When multiple measurements are made, take a secondary measurement for each child view as an exampleThe time complexity of the measurement algorithm for each View is O(2 n)

But what does Intrinsic Measurement do

Compose makes a single Intrinsic measurement of the entire component tree before making a formal measurement of the whole. By opening up two parallel measurement processes, we can avoid the constant doubling of measurement time caused by repeated measurement of the same subcomponent due to increasing levels. Down from O(2) to O(n)

Compose: how do you use Compose?

API too much, not one example, to point to the surface

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeStudyTheme { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Greeting() } } } } } @Composable fun Greeting() { var addCount by remember { mutableStateOf(0) } Column { Text( text = "Hello World $addCount", Clickable {addCount += 1}) for (I in 0 until addCount) {HelloWord(text = "I am ${I + 1}" ")}}} @composable fun HelloWord(Text: String) { Text(text = text) } @Preview(showBackground = true) @Composable fun DefaultPreview() { ComposeStudyTheme { Greeting() } }Copy the code

Compose brief principle

From the above demo, we can see Column, Text, @Composable, remember, etc., so let’s make an in-depth analysis

@Composable

Compose does not use an annotation processor. Compose works in the type checking and code generation phases of the Kotlin compilation plug-in, so it does not need an annotation processor. This annotation is more like a keyword, like the suspend keyword of the coroutine. Adding the @composable annotation changes the function type. The same function type is incompatible with unannotated and annotated functions.

@Composable
fun HelloWord(text: String) {
    Text(text = text)
}
Copy the code

The decompiled code is as follows:

public static final void HelloWord(String text, Composer $composer, int $changed) { int i; Composer $composer2; Intrinsics.checkNotNullParameter(text, "text"); Composer $composer3 = $composer.startRestartGroup(1404424604,"C(HelloWord)7@159L17:Hello.kt#nlh07n"); if (($changed & 14) == 0) { i = ($composer3.changed(text) ? 4 : 2) | $changed; } else { i = $changed; } if (((i & 11) ^ 2) ! = 0 | |! $composer3.getSkipping()) { $composer2 = $composer3; TextKt.m855Text6FffQQw(text, null, Color.m1091constructorimpl(ULong.m2915constructorimpl(0)), TextUnit.m2481constructorimpl(0), null, null, null, TextUnit.m2481constructorimpl(0), null, null, TextUnit.m2481constructorimpl(0), null, false, 0, null, null, $composer2, i & 14, 0, 65534); } else { $composer3.skipToGroupEnd(); $composer2 = $composer3; } ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup(); if (endRestartGroup ! = null) { endRestartGroup.updateScope(new HelloKt$HelloWord$1(text, $changed)); }}Copy the code

Composer is actually a context across the scope of the Composable function, providing methods for creating internal groups. The internal data structure is Slot Table

Slot Table is a type that stores data in contiguous space with an array implementation underneath. But the difference is its remaining space, called the Gap.

Gap has the ability to move to any region, making it more efficient at data insertion and deletion. Slot tables are linear data structures by nature, so View trees can be stored in Slot tables. The Slot Table’s ability to move insertion points allows the View tree to change without recreating the entire View tree’s data structure.

remeber

Remeber is a Composable function with a delegation-like internal implementation that implements object memory in a chain of calls to the Composable function. The Composable function calls remember to retrieve the contents of the last call with the chain position unchanged

  • The same Composable function is called in different locations and its remember function retrieves different contents
  • Multiple calls to the same Composable function generate multiple instances. Each call has its own life cycle

mutableStateOf

The real internal implementation of mutableStateOf is SnapshotMutableStateImpl

fun <T> mutableStateOf( value: T, policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy() ): MutableState<T> = createSnapshotMutableState(value, policy) internal actual fun <T> createSnapshotMutableState( value: T, policy: SnapshotMutationPolicy<T> ): SnapshotMutableState<T> = ParcelableSnapshotMutableState(value, policy) internal class ParcelableSnapshotMutableState<T>( value: T, policy: SnapshotMutationPolicy<T> ) : SnapshotMutableStateImpl<T>(value, policy), Parcelable { ... } internal open class SnapshotMutableStateImpl<T>( value: T, override val policy: SnapshotMutationPolicy<T> ) : StateObject, SnapshotMutableState<T> { override var value: T get() = next.readable(this).value set(value) = next.withCurrent { if (! policy.equivalent(it.value, value)) { next.overwritable(this, it) { this.value = value } } } }Copy the code

In the set method of SnapshotMutableStateImpl value, it completes the notification to the observer

Snapshot.kt

internal inline fun <T : StateRecord, R> T.overwritable( state: StateObject, candidate: T, block: T.() -> R ): R { var snapshot: Snapshot = snapshotInitializer return sync { snapshot = Snapshot.current this.overwritableRecord(state, snapshot, candidate).block() }.also { notifyWrite(snapshot, state) } } internal fun notifyWrite(snapshot: Snapshot, state: StateObject) { snapshot.writeObserver? .invoke(state) }Copy the code

Snapshot.current Obtains the current Snapshot. The scenario is as follows:

  • If the update is done asynchronously, since Snapshot is a ThreadLocal, the Snapshot of the current executing thread is returned
  • GlobalSnapshot is returned by default if the Snapshot of the current executing thread is empty
  • If mutableState is updated directly in the Composable, the Snapshot of the current Thread executing the Composable is the MutableSnapshot

Finally, the notifyWrite method is called to notify the observer

So the question is, when do you subscribe to an observer? Let’s go back to the setContent function and call it up to viewGroup.setContent

internal fun ViewGroup.setContent(
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    GlobalSnapshotManager.ensureStarted()
    ....
}

internal object GlobalSnapshotManager {
    fun ensureStarted() {
        ...
        Snapshot.registerGlobalWriteObserver {
                channel.trySend(Unit)
        }
    }
}
Copy the code

Snapshot.kt

fun registerGlobalWriteObserver(observer: ((Any) -> Unit)): ObserverHandle { sync { globalWriteObservers.add(observer) } advanceGlobalSnapshot() return ObserverHandle { sync { globalWriteObservers.remove(observer) } advanceGlobalSnapshot() } } private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T { val previousGlobalSnapshot = currentGlobalSnapshot.get() val result = sync { takeNewGlobalSnapshot(previousGlobalSnapshot, block) } // If the previous global snapshot had any modified states then notify the registered apply // observers. val modified = previousGlobalSnapshot.modified if (modified ! = null) { val observers: List<(Set<Any>, Snapshot) -> Unit> = sync { applyObservers.toMutableList() } observers.fastForEach { observer -> observer(modified, previousGlobalSnapshot) } } return result }Copy the code

The advanceGlobalSnapshot method notifies the Observer of the state change, and the above notifyWrite method notifies Compose, completing the UI driver

ComposeView

Compose doesn’t convert to a View when rendering, instead it has only one entry View, AndroidComposeView the Compose layout that we declared will be converted to NodeTree when we render, and AndroidComposeView will trigger the NodeTree layout and draw and basically, Compose has a View entry, but its layout and rendering is done on a LayoutNode, largely out of the View

public fun ComponentActivity.setContent( parent: CompositionContext? = null, content: @composable () -> Unit) {// If (existingComposeView! = null) with(existingComposeView) {setContent(content)} else ComposeView(this).apply {// will Compose // Add ComposeView to DecorView setContentView(this, DefaultActivityContentLayoutParams) } }Copy the code

In the setContent process, we’re going to create a ComposeView and an AndroidComposeView, which is the entry to Compose, And add it to the DecorView. AndroidComposeView in dispatchDraw will measure the layout and draw by going down through the child node by root. Here’s the entry for LayoutNode drawing on Android, Compose’s layout and drawing are largely separate from the View architecture, but still rely on Canvas

AndroidComposeView.kt

override fun dispatchDraw(canvas: Android.graphics.canvas) {// Compose measureAndLayout() // Compose drawInto(Canvas) {// Compose measureAndLayout() // Compose drawInto(Canvas) { root.draw(this) } ... } override fun measureAndLayout() { val rootNodeResized = measureAndLayoutDelegate.measureAndLayout() measureAndLayoutDelegate.dispatchOnPositionedCallbacks() }Copy the code

AndroidComposeView will measure the layout and draw through its children via root, and this is the entry point for LayoutNode drawing

What is a KMM

Before we get to KMM, let’s take a look at Compose Multiplatform

Compose Multiplatform

  • In December 2021, JetBrains released Compose Multiplatform 1.0

Compose Multiplatform can be seen as a superset of Jetpack Compose, extending the cross-platform capabilities of Jetpack Compose, and sharing most of the core common apis. Therefore, many of the base libraries for Compose Multiplatform still use androidx.compose. XXX as the package name, which makes it easy to port Android applications that are already implemented with Jetpack Compose to other platforms. The two have perfect interoperability.

Compose Multiplatform supports most of the major platforms on the market

INFOQ wechat official account

The Compose Multiplatform is composed by compiling the same code with different compilers to generate different artifacts on each end, which is cross-platform and compatible with the target platform. For example, Jar/AAR files are generated for JVM and Android platform through Kotlin/JVM, framework files are generated for IOS platform through Kotlin/Native, and Web platform through Kotlin/JS JavaScript files, which ultimately call the native API, make the adoption of the Compose Multiplatform not a performance drain and do not significantly increase the application size of Flutter.

Kotlin Multiplatform Mobile(KMM)

Compose Multiplatform’s cross-platform framework subset for Mobile is called Kotlin Multiplatform Mobile(KMM). Unlike Flutter, KMM does not want to use the same UI and code across all platforms. Unlike Flutter, KMM does not have a unified drawing engine built into it. Therefore, KMM supports multipurpose UI, but is weak. You still need to rely on the platform API when building the UI. KMM focuses on sharing a common set of business logic for all platforms below the UI layer, using Kotlin to write business code while maintaining interoperability with native development languages (Java, Objective-C, Swift, JavaScript, etc.). Flexibility while retaining the benefits of native programming.

The latter

Points to consider:

  1. How do you transition to Compose for your business?

Support native AndroidView and Compose mix

  1. For now, Compose has a long way to go. Should you learn and use it?

What is the future? I don’t know, but WHAT I do know is that everything Google pushes is becoming mainstream

  1. Think about China and Taiwan
  • KMM can solve the problem of unity at both ends and human resources
  • Often live broadcast medium focus more on the public logic layer, business how to write view is not too concerned about but need to be compatible
  1. What architectural development should Compose come with?
  • The MVVM, MVI?

“There is no best architecture, only the most appropriate architecture”

reference

  • Developer.android.com/jetpack/com…
  • Juejin. Cn/post / 684490…
  • Rengwuxian.com/tag/compose…