Moment For Technology

Android JectPack Compose

Posted on Dec. 2, 2022, 6:45 p.m. by Keith Lopez
Category: android Tag: android Android Jetpack

1. @Composable

A function with the @Composable annotation changes the type of the function, depending internally on the Composer throughout the function scope. @Composable features are as follows:

  • Composable is not an annotation handler per se. Compose relies on the Kotlin compiler plug-in during the Kotlin compiler's type detection and code generation phases, so you can use it without an annotation handler.

  • @Composable causes its type to change, and the same function type that is not annotated is incompatible with the annotated type

    @Composable assists the Kotlin compiler to know that this function is used to convert data into a UI that describes the display state of the screen

  • @Composable is not a language feature and cannot be implemented as a language keyword

Next, we analyze the internal implementation in terms of the simplest function

The kotlin code is as follows:

@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)[email protected]: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

There are a lot of conceptual points inside the function, so let's analyze them one by one.

2. Composer

Compose is actually the context throughout the Composable function scope, providing methods for creating internal groups and so on. The internal data structure is Slot Table, which has the following characteristics:

  • Slot Table is a type that stores data in continuous space, with an array implementation at the bottom. But the difference is the remaining space, called Gap.

    • Gap has the ability to move to any area, so it is more efficient during data insertion and deletion.
  • Slot tables are linear data structures by nature, so they support storing View trees in Slot tables.

    • According to the Slot Table can move the insertion point, so that the View tree after the change does not need to recreate the data structure of the whole View tree.
  • While Slot tables have the ability to insert data anywhere compared to ordinary arrays, Gap movement is still inefficient.

    • Google determines that in most cases the interface updates are data changes and that the View tree structure doesn't change very often.
    • And basically, only arrays, which are memory contiguous data structures, can meet the requirements of Compose Runtime in terms of access efficiency

Ps: All Composeable functions rely internally on Composer, so non-composeable functions cannot call Composeable functions

! [image-20210718173155867](/Users/dumengnan/Library/Application Support/typora-user-images/image-20210718173155867.png)

At this point, we try to deduce several results by backward inference:

  • If you use a linked list, the insertion time is o(1), but the lookup time is o(n),

  • If an array is used, the lookup time is o(1) and the insertion time is o(n) degrees.

    There are other solutions besides gap Buffer:

    1. Block linked lists (insertion and lookup can be done at O (n^1/2) complexity)
    2. Rope Tree (a balanced search tree)
    3. Piece Table (an improved version of the Gap Buffer used by Microsoft Doc, which also allows for fast undo and recombination)
  • The logical code is as follows:

    int GAP_SIZE = 5;// Default gap size
    
    // Basic structure
    struct GapBuffer{
    	char array[size];
    	int gapStart;
    	int gapSize;
    	int len;
    } gapBuffer;
    
    // Insert the logical implementation
    void insert(char c){
    	if(gapBuffer.gapSize0)
    		expanison();
    	gapBuffer.array[++gapBuffer.gapStart]=c;
    	--gapBuffer.gapSize;
    	++len;
    }
    
    // Logical expansion implementation
    void expanison(a){
      // Double the capacity
    	GAP_SIZE = GAP_SZIE*2;
    	gapBuffer.gapSize = GAP_SIZE;
      // Copy back the second half of the data, giving GAP_SiZE space
    	arraycopy(gapBuffer.array,gapBuffer.gapStart,gapBuffer.gapStart+gapBuffer.gapSize,len-gapBuffer.start);
    }
    
    // Move the gap logic implementation
    // The buffer will not be expanded
    void moveGap(int pos){
      // Do not move the same position
    	if(gapBuffer.gapStart == pos)return;
    	
    	// If pos is less than the current gap
    	if(posgapBuffer.gapStart)
        / / copy the array
    		arraycopy(gapBuffer.array,pos,pos+gapBuffer.gapSize,gapBuffer.gapStart-pos);
    	else
        / / copy the array
    		arraycopy(gapBuffer.array,gapBuffer.gapStart+gapBuffer.gapSize,gapBuffer.gapStart,gapBuffer.gapSize);
    	
    }
    // Array copy logic implementation
    void arraycopy(char array[].int srcSatrt,int dstStart,int len){
    	for(int i = 0; ilen; ++i)array[dstStart+i]=array[srcStart+i];
    }
    
    Copy the code
    • The load-bearing structure of THE UI is essentially a tree structure, and measurement, layout and rendering are all depth traversal of the UI tree.

2.1. Group creation and reorganization logic

2.1.1. The Group created

  • Groups are created according to the startRestartGroup and endRestartGroup methods and are finally created in the Slot Table
  • The Group is created for managementDynamic processing of UI(i.e.Data structure perspectivetheMove and insert)
    • The created Group lets the compiler know which developers' code will change which UI structure

2.1.2. Restructuring

The Compose Runtime determines which combinable functions need to be reexecuted based on the data impact scope, a step called regrouping

  • A Composable function can be recalled at any time, no matter how large the Composable structure is

  • By its nature, the Composable function does not recalculate the entire hierarchy when one part of it changes

  • Composer can determine specific calls based on whether the UI is being modified

    • Data updates cause part of the UI to refresh

    • scenario Composer code processing instructions
      Data updates cause part of the UI to refresh 1. Composer.skipToGroupEnd()

      Jump directly to the end of the current Group
      The non-refresh part is not recreated, butSkip redrawing, direct access
      2.composer.endrestartGroup () returns an object of type ScopeUpdateScope

      Finally, a Lambda is passed that calls the currently composable function
      Compose Runtime determines which functions can be composed based on the current environmentCall the range.

    PS:Composer performs separate read and write operations on the Slot Table. All written information is updated to the Slot Table only after the write operation is complete

The above only explains the reorganization of the upper layer of code calls, in fact, internal is dependent on State data State management, Positional Memoization


3. State

  • Compose is a declarative framework, and State uses the observer pattern to automatically update the interface with data

Simple function implementation that includes state management:

@Composable
fun Content(a) {
    var state by remember { mutableStateOf(1) }
    Column {
        Button(onClick = { state++ }) { Text(text = "click to change state")}
        Text("state value: $state")}}Copy the code

3.1. Remember ()

Remeber () is a Composable function with an internal implementation similar to a delegate that implements object memory in the Composable function call chain.

  • The Composable function is called without changing the location of the call chainremember()availableThe last callWhen the content of memory.
  • The same Composable function is called in different places, and its remember() function gets different content.

The same Composable function is called multiple times, resulting in multiple instances. Each call has its own lifecycle


3.2. mutableStateOf

  • The real internal implementation of mutableStateOf is SnapshotMutableStateImpl
fun T mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicyT = structuralEqualityPolicy()
): MutableStateT = SnapshotMutableStateImpl(value, policy)
Copy the code
  • In addition to the value that's passed in, there's the Policy
    • Processing policies are used to control the incoming data of mutableStateOf() and howTo report(observed timing), the strategy types are as follows:
      • structuralEqualityPolicy
      • ReferentialEqualityPolicy equality (strategy)
      • You can also customize the interface to implement the policy
private class SnapshotMutableStateImplT(
    value: T,
    override val policy: SnapshotMutationPolicyT
) : StateObject, SnapshotMutableStateT {
    @Suppress("UNCHECKED_CAST")
    override var value: T
        get() = next.readable(this).value
        set(value) = next.withCurrent {
            if(! policy.equivalent(it.value, value)) { next.writable(this) { this.value = value }
            }
        }
		// Current state Indicates the state
    private var next: StateStateRecordT = StateStateRecord(value)

    override val firstStateRecord: StateRecord
        get() = next

    override fun prependStateRecord(value: StateRecord) {
        @Suppress("UNCHECKED_CAST")
        next = value as StateStateRecordT
    }

    @Suppress("UNCHECKED_CAST")
    override fun mergeRecords(
        previous: StateRecord,
        current: StateRecord,
        applied: StateRecord
    ): StateRecord? {
        val previousRecord = previous as StateStateRecordT
        val currentRecord = current as StateStateRecordT
        val appliedRecord = applied as StateStateRecordT
        return if (policy.equivalent(currentRecord.value, appliedRecord.value))
            current
        else {
            val merged = policy.merge(
                previousRecord.value,
                currentRecord.value,
                appliedRecord.value
            )
            if(merged ! =null) {
                appliedRecord.create().also {
                    (it as StateStateRecordT).value = merged
                }
            } else {
                null}}}private class StateStateRecordT(myValue: T) : StateRecord() {
        override fun assign(value: StateRecord) {
            @Suppress("UNCHECKED_CAST")
            this.value = (value as StateStateRecordT).value
        }

        override fun create(a): StateRecord = StateStateRecord(value)

        var value: T = myValue
    }
}
Copy the code
  • SnapshotMutableStateImpl internally performs some processing of writing to Composer (data comparison, data merge, read, write, etc.)

    PS: The logic for processing is based on the strategy described above

3.2.1. Data notification

In a separate set() method for SnapshotMutableStateImpl, observer notification is completed for it. The specific process is as follows:

3.2.1.1 get Snapshot
inline fun T : StateRecord, R T.writable(state: StateObject, block: T. () - R): R {
    var snapshot: Snapshot = snapshotInitializer
    return sync {
        snapshot = Snapshot.current
        this.writableRecord(state, snapshot).block()
    }.also {
        notifyWrite(snapshot, state)
    }
}
Copy the code

Block is called to directly control writability in the first state record

  • Snapshot.current Obtains the current Snapshot. The scenario is described as follows:
    • If updated asynchronously, the Snapshot is a ThreadLocal, so the Snapshot of the current executing thread is returned
    • If the Snapshot of the current thread is empty, GlobalSnapshot is returned by default
    • If you update the mutableState directly in the Composable, the Snapshot of the current executing thread in the Composable is a MutableSnapshot
3.2.1.2. Control write | save to Modified
  • Once the snapshot is obtained, write to it
internal fun T : StateRecord T.writableRecord(state: StateObject, snapshot: Snapshot): T {
    ........
    snapshot.recordModified(state)
    return newData
}
Copy the code
  • Finally, recordModified is used to implement writing
override fun recordModified(state: StateObject){ (modified ? : HashSetStateObject().also { modified = it }).add(state) }Copy the code
  • Add the current modified state to modified of the current Snapshot
3.2.1.3. Observer notification
internal fun notifyWrite(snapshot: Snapshot, state: StateObject){ snapshot.writeObserver? .invoke(state) }Copy the code
  • Finally, the notifyWrite method is called to complete the notification of the observer
3.2.1.4. Differences between Kotlin functions
function The structure of the body Function object The return value Extension function or not scenario
let fun T, R T.let(block: (T) - R): R = block(this) It == Current object closure is Not null
with fun T, R with(receiver: T, block: T.() - R): R = receiver.block() This == current object closure no Call multiple methods of the same class

You can simply call the methods of the class without the class name
run fun T, R T.run(block: T.() - R): R = block() This == current object closure is Any scenario of the let,with function
apply fun T.apply(block: T.() - Unit): T { block(); return this } This == current object this is Any scenario in which the run function, while initializing the instance, directly operates on the property and returns

Views with the dynamic Inflate XML bind data at the same time

Multiple extension function chained calls

The problem of data model multi - level package void processing
also fun T.also(block: (T) - Unit): T { block(this); return this } It == Current object this is Applies to any scenario of a let function, and can generally be used for chained calls to multiple extension functions

3.2.2. Observer registration

  • In the first place in the external setContent () method invocation of the GlobalSnapshotManager. EnsureStarted () method
internal fun ViewGroup.setContent(
    parent: CompositionContext,
    content: @Composable() - Unit
): Composition {
    GlobalSnapshotManager.ensureStarted()
    ....
}
Copy the code
  • EnsureStarted internally registered a global globalWriteObserver
fun ensureStarted(a) {
    if (started.compareAndSet(false.true)) {
        removeWriteObserver = Snapshot.registerGlobalWriteObserver(globalWriteObserver)
    }
}
Copy the code

Started is AtomicBoolean, which essentially uses the CPU's CAS instruction to ensure atomicity. Because it is cpu-level instruction, it is less expensive than locking that requires the operating system to participate.

  • Next let's look at the Implementation of globalWriteObserver
private val globalWriteObserver: (Any) - Unit = {
    if(! commitPending) { commitPending =true
        schedule {
            commitPending = false
            Snapshot.sendApplyNotifications()
        }
    }
}
Copy the code
  • Compose ignores multiple schedules, internally uses the CallBackList as a monitor lock, and eventually synchronously executes its invoke (unfinished) and enters the update state.
private fun schedule(block: () - Unit) {
    synchronized(scheduledCallbacks) {
        scheduledCallbacks.add(block)
        if(! isSynchronizeScheduled) { isSynchronizeScheduled =true
            scheduleScope.launch { synchronize() }
        }
    }
}
private fun synchronize(a) {
        synchronized(scheduledCallbacks) {
            scheduledCallbacks.forEach { it.invoke() }
            scheduledCallbacks.clear()
            isSynchronizeScheduled = false}}Copy the code
  • Will call the Snapshot. SendApplyNotifications ()
fun sendApplyNotifications(a) {
    valchanges = sync { currentGlobalSnapshot.modified? .isNotEmpty() ==true
    }
    if (changes)
        advanceGlobalSnapshot()
}
Copy the code

The modified version of this method is also at the heart of Compse and is the focus of implementing the extension feature, which is discussed below

  • When Modified is not empty, advanceGlobalSnapshot is called
private fun T advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) - T): T {
    val previousGlobalSnapshot = currentGlobalSnapshot
    val result = sync {
        takeNewGlobalSnapshot(previousGlobalSnapshot, block)
    }

    val modified = previousGlobalSnapshot.modified
    if(modified ! =null) {
        val observers = sync { applyObservers.toList() }
        for (observer in observers) {
            observer(modified, previousGlobalSnapshot)
        }
    }
		.....
    return result
}
Copy the code

This method notifies the observer of the value of the state change, and the notifyWrite method above notifies the Compose, completing the UI driver

Next, let's look at which callbacks are available


Reorganization of 3.2.3.

The source code is quite large here, so I'll just summarize it from the figure above

  • Reorganizations occur every time a status update occurs. The Composable function must be explicitly informed of the new state before it can update accordingly.
  • ApplyObservers, as proposed above, actually includes two observers, and the conclusion is as follows:
    • SnapshotStateObserver. ApplyObserver: used to update the Snapshot
    • RecompositionRunner: Handles the reorganization process

Drawing 3.3.4.

  • At this point, Compose completes the creation of the View tree and contains the hosted data, but the rendering of Compose is independent of the creation
  1. The Composable function does not have to run only on the main thread
  2. Regrouping is an optimistic operation. If the data-driven event is completed before the regrouping is complete, the regrouping will be cancelled, so the result should be written in such a way that it is idempotent.

Render 3.4.5.

  • Rendering ends up calling the ReusableComposeNode() method to create the LayoutNode as the View node

The LayoutNode is a bit like the elements of the Flutter, which together make up the View tree

  • AndroidComposeView is the underlying dependency of Compose, which has a CanvasHolder inside it

    • CanvasHolder android. Graphics. Canvas agent into androidx.com pose. The UI. Graphics. Canvas, eventually to LayoutNode used in a variety of drawing

    The conclusion here is only a summary conclusion. There are many differences in the implementation of many specific elements, but their essence is Canvas proxy

    The other thing is that Compose is platform-independent, which is for greater platform compatibility

4. Measurement of inherent characteristics

  • Instead of measuring multiple times in Android's traditional UI system, Compose measures only once
  • If you need to rely on the subview measurement information, you can passMeasurement of natural propertiesObtain the inherent characteristics of the subview measurement information, and then the actual measurement
    • (min | Max) IntrinsicWidth: given the minimum/maximum width of the View
    • (min | Max) IntrinsicHeight: the minimum/maximum height of a given View
  • Android traditional UI system measurement time complexity: O(2n) n=View hierarchy depth 2= parent View to child View measurement times
    • The View level is increased, and the measurement times are doubled
  • Intrinsic characteristic measurement can obtain the width and height information of each child View in advance before the parent View measurement, so as to calculate its own width and height
    • In some scenarios where the parent View is not required to participate in the calculation, but the child View directly influences each other through the measurement sequence, SubcomposeLayout can be used to deal with the scenario where there is a dependency relationship between the child View

5. To summarize

Problem of 5.1.

  1. Dynamic display of the UI: Changing the UI during execution requires constant validation and ensuring its dependencies. Also ensure dependency satisfaction throughout the lifecycle.
  2. Tight coupling: Code in one place affects many places, and in most cases it is implicit, seemingly unrelated, but actually has an impact
  3. Imperative UI: When writing UI code, always think about how to transition to the corresponding state
  4. Single inheritance: Are there other ways to break through the limitations of single inheritance?
  5. Code bloat: How can you control code bloat as your business expands?

5.2. Separation of concerns

  • Thinking about Separation of concerns (SOC) in terms of cohesion and coupling
    • Coupling: Dependencies between elements in different modules that affect each other
      • Tight coupling: Code in one place affects many places, and in most cases it is implicit, seemingly unrelated, but actually has an impact
    • Cohesion: The relationship between the elements of a module, the reasonable degree to which the elements of a module are combined with each other
      • Combine related code together as much as possible, and expand the code as it grows

5.3. Composable functions

  • The Composable function can be a conversion function for the data. You can use any Kotlin code to take this data and use it to describe the hierarchy
  • When other Composable functions are called, these calls represent the UI in our hierarchy. And you can use language-level primitives in Kotlin to dynamically perform various operations.
    • For example, you can use if statements and for loops to implement control flow to handle UI logic.
  • Make use of Kotlin's trailing lambda syntax to implement the Composable function of the Composable lambda parameter, that is, the Composable function pointer

5.4. Declarative UI

  • Write code that describes the UI as it is, not how to transition to the corresponding state.
  • You don't care what state the UI was in before, you just specify what state it should be in now.
  • Compose controls how you go from one state to another, so you don't have to worry about state transitions anymore.

5.5. Encapsulation

  • The Composable function manages and creates the state, and then passes that state and any data it receives as parameters to the other Composable functions.

Reorganization of 5.6.

  • A Composable function can be recalled at any time, no matter how large the Composable structure is
  • By its nature, the Composable function does not recalculate the entire hierarchy when one part of it changes

5.7. observeAsState

  • The observeAsState method maps LiveData to State and uses its value in the Scope of the function body.
  • The State instance subscribes to the LiveData instance, and the State is updated wherever the LiveData changes.
  • These changes are automatically subscribed to wherever the State instance is read, the already included code, or the Composable function that has been read.
  • Instead of specifying LifecycleOwner or update callbacks, the Composable can implement both implicitly.

Combination of 5.8.

  • Different from traditional inheritance, combination can combine a series of simple code into a complex code logic, breaking through the limitation of single inheritance.

    • Make use of Kotlin's trailing lambda syntax to implement the Composable function of the Composable lambda parameter, that is, the Composable function pointer
Search
About
mo4tech.com (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.