Jetpack Compose-状态订阅与自动更新

2022-11-24/2022-11-24

Compose的底层原理

Compose 的UI结构

decorView
	LinearLayout
		android.R.id.content
			ComposeView
				AndroidComposeView
					LayoutNode
					/		\
				LayoutNode	LayoutNOde
				......		......

从setContent看起 主要以content为主线分析

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    //content就是我们写的UI内容
    content: @Composable () -> Unit
) {
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    //existingComposeView默认为null
    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
        //CompositionContext默认为null
        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
        //修bug 可以不关注
        setOwners()
        //将ComposeView Set进View系统
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}
class ComposeView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {

    private val content = mutableStateOf<(@Composable () -> Unit)?>(null)

    /**
     * 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
        //最后不管怎么会在AttachedToWindow的时候调用ensureCompositionCreated
        if (isAttachedToWindow) {
            createComposition()
        }
    }
}
#AbstractComposeView
@Suppress("DEPRECATION") // Still using ViewGroup.setContent for now
    private fun ensureCompositionCreated() {
        //默认composition == null
        if (composition == null) {
            try {
                creatingComposition = true
                //在resolveParentCompositionContext()获得了CompositionContext
                //默认是一个windowRecomposer对象
                composition = setContent(resolveParentCompositionContext()) {
                    Content()
                }
            } finally {
                creatingComposition = false
            }
        }
    }

    /**
     * Determine the correct [CompositionContext] to use as the parent of this view's
     * composition. This can result in caching a looked-up [CompositionContext] for use
     * later. See [cachedViewTreeCompositionContext] for more details.
     *
     * If [cachedViewTreeCompositionContext] is available but [findViewTreeCompositionContext]
     * cannot find a parent context, we will use the cached context if present before appealing
     * to the [windowRecomposer], as [windowRecomposer] can lazily create a recomposer.
     * If we're reattached to the same window and [findViewTreeCompositionContext] can't find the
     * context that [windowRecomposer] would install, we might be in the [getOverlay] of some
     * part of the view hierarchy to animate the disappearance of this and other views. We still
     * need to be able to compose/recompose in this state without creating a brand new recomposer
     * to do it, as well as still locate any view tree dependencies.
     */
//自己没有就从下往上找,默认会一直找不到就返回一个windowRecomposer对象
    private fun resolveParentCompositionContext() = parentContext
        ?: findViewTreeCompositionContext()?.cacheIfAlive()
        ?: cachedViewTreeCompositionContext?.get()?.takeIf { it.isAlive }
        ?: windowRecomposer.cacheIfAlive()
/**
 * Composes the given composable into the given view.
 *
 * The new composition can be logically "linked" to an existing one, by providing a
 * [parent]. This will ensure that invalidations and CompositionLocals will flow through
 * the two compositions as if they were not separate.
 *
 * Note that this [ViewGroup] should have an unique id for the saved instance state mechanism to
 * be able to save and restore the values used within the composition. See [View.setId].
 *
 * @param parent The [Recomposer] or parent composition reference.
 * @param content Composable that will be the content of the view.
 */
internal fun AbstractComposeView.setContent(
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    GlobalSnapshotManager.ensureStarted()
    //找到AndroidComposeView 如果没有就创建
    //从这里可以看出ComposeView的下面是一个AndroidComposeView
    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)
    //这里也是尝试找WrappedComposition 找不到就新创建一个
    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
}
private class WrappedComposition(
    val owner: AndroidComposeView,
    val original: Composition
) : Composition, LifecycleEventObserver {

    private var disposed = false
    private var addedToLifecycle: Lifecycle? = null
    private var lastContent: @Composable () -> Unit = {}

    override fun setContent(content: @Composable () -> Unit) {
        owner.setOnViewTreeOwnersAvailable {
            if (!disposed) {
                val lifecycle = it.lifecycleOwner.lifecycle
                lastContent = content
                if (addedToLifecycle == null) {
                    addedToLifecycle = lifecycle
                    // this will call ON_CREATE synchronously if we already created
                    lifecycle.addObserver(this)
                } else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
                    //这里的original是
                    //val original = Composition(UiApplier(owner.root), parent)
                    //Composition函数会返回一个CompositionImpl对象
                    original.setContent {

                        @Suppress("UNCHECKED_CAST")
                        val inspectionTable =
                            owner.getTag(R.id.inspection_slot_table_set) as?
                                MutableSet<CompositionData>
                                ?: (owner.parent as? View)?.getTag(R.id.inspection_slot_table_set)
                                    as? MutableSet<CompositionData>
                        if (inspectionTable != null) {
                            inspectionTable.add(currentComposer.compositionData)
                            currentComposer.collectParameterInformation()
                        }

                        LaunchedEffect(owner) { owner.keyboardVisibilityEventLoop() }
                        LaunchedEffect(owner) { owner.boundsUpdatesEventLoop() }

                        CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
                            ProvideAndroidCompositionLocals(owner, content)
                        }
                    }
                }
            }
        }
    }

}
//CompositionImpl


//即为WindowReComposer
    private val parent: CompositionContext,

    override fun setContent(content: @Composable () -> Unit) {
        check(!disposed) { "The composition is disposed" }
        this.composable = content
        //
        parent.composeInitial(this, composable)
    }
//Recomposer
    internal override fun composeInitial(
        composition: ControlledComposition,
        content: @Composable () -> Unit
    ) {
        val composerWasComposing = composition.isComposing
        composing(composition, null) {
            composition.composeContent(content)
        }
        // TODO(b/143755743)
        if (!composerWasComposing) {
            Snapshot.notifyObjectsInitialized()
        }

        synchronized(stateLock) {
            if (_state.value > State.ShuttingDown) {
                if (composition !in knownCompositions) {
                    knownCompositions += composition
                }
            }
        }

        composition.applyChanges()

        if (!composerWasComposing) {
            // Ensure that any state objects created during applyChanges are seen as changed
            // if modified after this call.
            Snapshot.notifyObjectsInitialized()
        }
    }
//CompositionImpl
override fun composeContent(content: @Composable () -> Unit) {
        // TODO: This should raise a signal to any currently running recompose calls
        // to halt and return
        trackAbandonedValues {
            synchronized(lock) {
                drainPendingModificationsForCompositionLocked()
                composer.composeContent(takeInvalidations(), content)
            }
        }
    }

//Composer
    internal fun composeContent(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
        content: @Composable () -> Unit
    ) {
        runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
        doCompose(invalidationsRequested, content)
    }

    private fun doCompose(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
        content: (@Composable () -> Unit)?
    ) {
        runtimeCheck(!isComposing) { "Reentrant composition is not supported" }
        trace("Compose:recompose") {
            snapshot = currentSnapshot()
            invalidationsRequested.forEach { scope, set ->
                val location = scope.anchor?.location ?: return
                invalidations.add(Invalidation(scope, location, set))
            }
            invalidations.sortBy { it.location }
            nodeIndex = 0
            var complete = false
            isComposing = true
            try {
                startRoot()
                // Ignore reads of derivedStateOf recalculations
                observeDerivedStateRecalculations(
                    start = {
                        childrenComposing++
                    },
                    done = {
                        childrenComposing--
                    },
                ) {
                    if (content != null) {
                        startGroup(invocationKey, invocation)

                        invokeComposable(this, content)
                        endGroup()
                    } else {
                        skipCurrentGroup()
                    }
                }
                endRoot()
                complete = true
            } finally {
                isComposing = false
                invalidations.clear()
                providerUpdates.clear()
                if (!complete) abortRoot()
            }
        }
    }

//在这里最终执行了content
internal fun invokeComposable(composer: Composer, composable: @Composable () -> Unit) {
    @Suppress("UNCHECKED_CAST")
    val realFn = composable as Function2<Composer, Int, Unit>
    realFn(composer, 1)
}

MutableState&mutableStateOf()

Compose是声明式UI的框架,意味着数据的改变可以直接影响到UI

而不需要像命令式UI那样数据改变后需要手动调用来改变UI

这需要靠MutableState来实现数据订阅功能

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)

ParcelableSnapshotMutableState只是定义了一些Parcal用来跨进程传输的格式,真正实现订阅功能的是它的父类SnapshotMutableStateImpl

/**
 * A single value holder whose reads and writes are observed by Compose.
 *
 * Additionally, writes to it are transacted as part of the [Snapshot] system.
 *
 * @param value the wrapped value
 * @param policy a policy to control how changes are handled in a mutable snapshot.
 *
 * @see mutableStateOf
 * @see SnapshotMutationPolicy
 */
internal open class SnapshotMutableStateImpl<T>(
    value: T,
    override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
    @Suppress("UNCHECKED_CAST")
    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 }
            }
        }

    private var next: StateStateRecord<T> = StateStateRecord(value)

    override val firstStateRecord: StateRecord
        get() = next

    private class StateStateRecord<T>(myValue: T) : StateRecord() {
        override fun assign(value: StateRecord) {
            @Suppress("UNCHECKED_CAST")
            this.value = (value as StateStateRecord<T>).value
        }

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

        var value: T = myValue
    }

}

记录数据的数据结构是StateObject,它是一个链表结构,这是因为Compose支持事务功能,这意味着Compose是可以撤回操作的,所以需要记录旧的值

跟入value的get函数

fun <T : StateRecord> T.readable(state: StateObject): T =
    readable(state, currentSnapshot())

/**
 * Return the current readable state record for the [snapshot]. It is assumed that [this]
 * is the first record of [state]
 */
fun <T : StateRecord> T.readable(state: StateObject, snapshot: Snapshot): T {
    // invoke the observer associated with the current snapshot.
    //在这里进行订阅
    snapshot.readObserver?.invoke(state)
    return readable(this, snapshot.id, snapshot.invalid) ?: readError()
}

private fun <T : StateRecord> readable(r: T, id: Int, invalid: SnapshotIdSet): T? {
    // The readable record is the valid record with the highest snapshotId
    var current: StateRecord? = r
    var candidate: StateRecord? = null
    while (current != null) {
        if (valid(current, id, invalid)) {
            candidate = if (candidate == null) current
            else if (candidate.snapshotId < current.snapshotId) current else candidate
        }
        current = current.next
    }
    if (candidate != null) {
        @Suppress("UNCHECKED_CAST")
        return candidate as T
    }
    return null
}

重组作用域和remember函数

image-20221119185516579

这样写时发现过了三秒钟后name并没有被更改

报红只是不建议这里这样写,是不会导致程序崩溃的

原因是由于Compose中的重组作用域,当数据发生变化时,重组作用域中的代码会被再次执行以保证数据和UI的绑定是正确的

在上图中的name和Text在一个重组作用域中

所以在上图中name发生变化时,name会被重新初始化一次,所以导致了数据没有被刷新

那怎么解决了,最简单的解决办法就是把name移到重组作用域之外

image-20221119190627208

还可以让name这个mutableState和Text不在同一个重组作用域中

@Composable
fun rememberLearn() {

    var name by remember { mutableStateOf("okandgreat") }

    Button(onClick = {}) {
        Text(name)
    }


    GlobalScope.launch {
        delay(2000)
        name = "greatandok"
    }
}

还有一种办法就是使用remember函数

image-20221119191112599

remrember:起缓存作用,可以防止多次初始化

第一次初始化时会执行block,然后会取旧值

remrember函数还可以用来防止多次初始化

它有一个带参数的重载函数

image-20221119191506540

假如这个地方我们使用的是上面那个形式,当value没有发生变化时,仍然会重复计算

而在下面使用了remrember函数后,由于我们提供了一个key,当下一次value发生变化时,会去检查key是否发生了变化,如果没有的话就会直接取旧值而不会重复计算

如果涉及到多个值可以在填多个参数,他们是或的关系

image-20221119191951259

PS:这里协程的使用方式报错是由于会报错

Calls to launch should happen inside a LaunchedEffect and not composition

在compose里有专门提供的协程

此外值得一提的是这个地方由于重组的运行机制会一直重复执行

重组中的记忆功能

如果在初始组合期间或重组期间调用了可组合函数,则认为其存在于组合中。未调用的可组合函数(例如,由于该函数在 if 语句内调用且未满足条件)不存在于组合中。

remember 会将对象存储在组合中,而如果在重组期间未再次调用之前调用 remember 的来源位置,则会忘记对象。

为了直观呈现这种行为,将在应用中实现以下功能:当用户至少饮用了一杯水时,向用户显示有一项待执行的健康任务,同时用户也可以关闭此任务

class StateActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colors.background
            ) {
                WellnessScreen()
            }
        }
    }

    @Composable
    fun WellnessScreen(modifier: Modifier = Modifier) {
        WaterCounter(modifier)
    }


    /**
     * 优秀实践是为所有可组合函数提供默认的 Modifier,从而提高可重用性。
     * 它应作为第一个可选参数显示在参数列表中,位于所有必需参数之后。
     */
    @Composable
    fun WaterCounter(modifier: Modifier = Modifier) {
        Column(modifier = modifier.padding(16.dp)) {
            var count by remember { mutableStateOf(0) }
            if (count > 0) {
                var showTask by remember { mutableStateOf(true) }
                if (showTask) {
                    WellnessTaskItem(
                        onClose = { showTask = false },
                        taskName = "Have you taken your 15 minute walk today?"
                    )
                }
                Text("You've had $count glasses.")
            }

            Row(Modifier.padding(top = 8.dp)) {
                Button(onClick = { count++ }, enabled = count < 10) {
                    Text("Add one")
                }
                Button(onClick = { count = 0 }, Modifier.padding(start = 8.dp)) {
                    Text("Clear water count")
                }
            }
        }
    }

    @Composable
    fun WellnessTaskItem(
        taskName: String,
        onClose: () -> Unit,
        modifier: Modifier = Modifier
    ) {
        Row(
            modifier = modifier, verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                modifier = Modifier.weight(1f).padding(start = 16.dp),
                text = taskName
            )
            IconButton(onClick = onClose) {
                Icon(Icons.Filled.Close, contentDescription = "Close")
            }
        }
    }
}

运行应用时,屏幕会显示初始状态:

img

组件树示意图,显示了应用的初始状态,计数为 0

组件树示意图,显示了应用的初始状态,计数为 0

  • 按下 Add one 按钮。此操作会递增 count(这会导致重组),并同时显示 WellnessTaskItem 和计数器 Text

9257d150a5952931.png

组件树示意图,显示了状态变化,当用户点击 Add one 按钮时,系统会显示包含提示的文本和包含饮水杯数的文本。

  • 按下 WellnessTaskItem 组件的 X(这会导致另一项重组)。showTask 现在为 false,这意味着不再显示 WellnessTaskItem

    6bf6d3991a1c9fd1.png

    • 按下 Add one 按钮(另一项重组)。如果您继续增加杯数,showTask 会记住您在下一次重组时关闭了 WellnessTaskItem。因此此时虽然count>0 但不会显示taskItem

    img

    img

  • 按下 Clear water count 按钮可将 count 重置为 0 并导致重组。系统不会调用显示 countText 以及与 WellnessTaskItem 相关的所有代码,并且会退出组合。

    ae993e6ddc0d654a.png

  • 由于系统未调用之前调用 showTask 的代码位置,因此会忘记 showTask。这将返回第一步。

    img

img

  • 按下 Add one 按钮,使 count 大于 0,此时会发生重组。

9257d150a5952931.png

  • 系统再次显示 WellnessTaskItem 可组合项,因为在退出上述组合时,之前的 showTask 值已被忘记。

可以看出这其实是反直觉的,我们很容易忽略showTask在Count被清零后由于没有调用If(count > 0) 中的代码从而状态丢失

解决办法:使用viewmodel来储存状态

导库

def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
    implementation(composeBom)
    androidTestImplementation(composeBom)
	implementation "androidx.compose.runtime:runtime-livedata"

使用:

class StateViewModel : ViewModel() {
    private val _showTask = mutableStateOf(true)

    var showTask: Boolean
        get() = _showTask.value
        private set(value) {}

    fun notShowTask(){
        _showTask.value = false
    }
}


class StateActivity : ComponentActivity() {

    lateinit var viewModel: StateViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            viewModel = viewModel()
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colors.background
            ) {
                WellnessScreen()
            }
        }
    }
    @Composable
    fun WaterCounter(modifier: Modifier = Modifier) {
        Column(modifier = modifier.padding(16.dp)) {
            var count by remember { mutableStateOf(0) }
            if (count > 0) {

                val showTask = viewModel.showTask
                if (showTask) {
                    WellnessTaskItem(
                        onClose = { viewModel.notShowTask() },
                        taskName = "Have you taken your 15 minute walk today?"
                    )
                }
                Text("You've had $count glasses.")
            }

            Row(Modifier.padding(top = 8.dp)) {
                Button(onClick = { count++ }, enabled = count < 10) {
                    Text("Add one")
                }
                Button(onClick = { count = 0 }, Modifier.padding(start = 8.dp)) {
                    Text("Clear water count")
                }
            }
        }
    }
}

无状态&状态提升

在命令式UI中,比如TextView,我们可以通过setText()设置文字,getText()获取文字,这个文字可以看作TextView的状态

而在Compose中,元素的状态完全由传递给它们的参数控制,这样可确保单一可信的来源,声明式UI的重要理念是,程序员负责描述给定状态的界面外观,而框架负责在状态更改时更新界面

使用 remember 存储对象的可组合项会创建内部状态,使该可组合项有状态。在调用方不需要控制状态,并且不必自行管理状态便可使用状态的情况下,“有状态”会非常有用。但是,具有内部状态的可组合项往往不易重复使用,也更难测试。

无状态可组合项是指不保持任何状态的可组合项。实现无状态的一种简单方法是使用状态提升

在开发可重复使用的可组合项时,您通常想要同时提供同一可组合项的有状态和无状态版本。有状态版本对于不关心状态的调用方来说很方便,而无状态版本对于需要控制或提升状态的调用方来说是必要的。

状态提升是一种将状态移至可组合项的调用方以使可组合项无状态的模式。

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

通过从 HelloContent 中提升出状态,使得该组件更容易被复用

状态下降、事件上升的这种模式称为“单向数据流”。在这种情况下,状态会从 HelloScreen 下降为 HelloContent,事件会从 HelloContent 上升为 HelloScreen。通过遵循单向数据流,您可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分解耦。

我们要遵循一个原则:状态尽量能不提就不提,因为状态暴露的范围扩大出错的概率也会增大

重组的性能风险和优化

看一下这串代码

image-20221120121815038

当点击Column里的Text时会输出

ReComposeScopeLearn: 1
ReComposeScopeLearn: 2
ReComposeScopeLearn: 4

输出

ReComposeScopeLearn: 2
ReComposeScopeLearn: 4

很容易理解,因为当数据发生变化时Compose需要发生重组,但为什么会输出ReComposeScopeLearn: 1呢?

我们点击Column的源码

@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

@Suppress("ComposableLambdaParameterPosition")
@Composable inline fun Layout(
    content: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current
    val viewConfiguration = LocalViewConfiguration.current
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
        },
        skippableUpdate = materializerOf(modifier),
        content = content
    )
}

@Composable @ExplicitGroupsComposable
inline fun <T, reified E : Applier<*>> ReusableComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater<T>.() -> Unit,
    noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
    content: @Composable () -> Unit
) {
    if (currentComposer.applier !is E) invalidApplier()
    currentComposer.startReusableNode()
    if (currentComposer.inserting) {
        currentComposer.createNode(factory)
    } else {
        currentComposer.useNode()
    }
    currentComposer.disableReusing()
    Updater<T>(currentComposer).update()
    currentComposer.enableReusing()
    SkippableUpdater<T>(currentComposer).skippableUpdate()
    currentComposer.startReplaceableGroup(0x7ab4aae9)
    //我们写的content在这里
    content()
    currentComposer.endReplaceableGroup()
    currentComposer.endNode()
}

可以发现全部为内联函数,也就是说输出ReComposeScopeLearn: 1的代码和输出2,4的其实是没有大括号隔离的,它们是在同样的作用域里,因此数据源发生变法导致重组时这个重组作用域内的代码都会重新执行

这就带来一个问题:重组作用域过大会导致性能损耗

重新看刚才那些代码,会发现ReComposeScopeLearn: 3没有被重新执行啊?

这就是Compose对重组可能导致的性能风险进行的优化,当发生重组时,Compose会检查函数是否为Stable的,如果是Stable的,那么会对函数参数进行结构性相等的检查,通过则不会进入Composable函数,如果不是Stable的,则一定会进入该Composable函数

那什么样的函数是Stable的呢?有这么三条规则

*   1) The result of [equals] will always return the same result for the same two instances.
*   2) When a public property of the type changes, composition will be notified.
*   3) All public property types are stable.

而Compose只会对第二条进行检查,如果第二条通过,则这个函数被认为是Stable的,反之则不是

因此下面这个代码会输出ReComposeScopeLearn: 5

@Composable
fun ReComposeScopeLearn() {

    var name by remember { mutableStateOf("okandgreat") }
    val user = User("okandgreat")

    Log.d(TAG, "ReComposeScopeLearn: 1")

    Column {
        Log.d(TAG, "ReComposeScopeLearn: 2")
        Text(name, Modifier.clickable {
            name = "greatandok"
        })
        Scope3(user)
    }
}

@Composable
fun Scope3(user: User) {
    Text("scope3 ${user.name}")
    Log.d(TAG, "ReComposeScopeLearn: 5")
}

data class User(var name: String) {}

但是把

data class User(var name: String) {}

改为

data class User(val name: String) {}

又不会输出ReComposeScopeLearn: 5了

因为此时Compose认为Scope3函数满足Stable函数的第二条规定,即When a public property of the type changes, composition will be notified

为什么改成var就可能出问题呢?考虑如下代码:

@Composable
fun ReComposeScopeLearn() {

    var name by remember { mutableStateOf("okandgreat") }

    Log.d(TAG, "ReComposeScopeLearn: 1")

    var user1 = User("okandgreat")
    var user2 = User("okandgreat")
    var user = user1

    Column {
        Log.d(TAG, "ReComposeScopeLearn: 2")
        Text(name, Modifier.clickable {
            name = "greatandok"
            user = user2
        })
        Scope1()
        Scope2(name)
        Scope3(user)
    }
}

@Composable
fun Scope3(user: User) {
    Text("scope3 ${user.name}")
    Log.d(TAG, "ReComposeScopeLearn: 5")
}

data class User(var name: String) {}

当Text被点击时user被指向了user2,在代码执行到Scope3(user)触发结构性检测没有问题因此scope3的代码不会被执行,而如果后面我们在某个地方把user2.name改成了一个其它的值并且Scope内部发生了重组,因为之前我们由于优化没有进入Scope3,因此Scope内部的User还是指向user1,此时就发生了错误

而出现这个问题的原因是改成var后我们就没办法保证user1和user2能时刻相等了!

我们可以这样解决以满足第二条规定

class User(name: String) {
    val name by mutableStateOf(name)
}

还可以使用@Stable注解来强制认为这是一个Stable的函数

@Composable
fun Scope3(user: User) {
    Text("scope3 ${user.name}")
    Log.d(TAG, "ReComposeScopeLearn: 5")
}

@Stable
data class User(var name: String) {}

但这样就可能会发生我们刚刚所说的我们没办法保证user1和user2能时刻相等所了

为了保证这一点,我们可以保证User类只要不是同一个内存中的对象就让它们的equals返回false

只要使用Object类的Equasl就可以了

@Stable
class User(var name: String) {}

此外,除了@Stable注解 使用@Immutable注解也可以达到同样的效果

DerivedState

DerivedState可以在内部所有使用的状态变量发生改变时进行重新计算

以以下代码为例:

/**
 * Use derivedStateOf when a certain state is calculated or derived from other state objects.
 * Using this function guarantees that the calculation will only occur whenever one of the states used in the calculation changes.
 */
@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {

    val todoTasks = remember { mutableStateListOf<String>() }

    // Calculate high priority tasks only when the todoTasks or highPriorityKeywords
    // change, not on every recomposition
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}

当todoTasks或highPriorityKeywords发生改变时

todoTasks.filter { it.containsWord(highPriorityKeywords) }会重新计算,最终导致highPriorityTasks的值发生变化

那么我们是在一个值依赖于其它值时使用DerivedState吗?如果是这种情况的话我们为什么不使用带参数的remember函数呢?

var name by remember { mutableStateOf("okandgreat") }
    val processedName by remember(name) { derivedStateOf { name.uppercase() } }
    val procrssedNameWithoutDerivedState = remember(name) { name.uppercase() }
    Text(processedName, Modifier.clickable { name = "greatandok" })

好像没有任何问题

那如果是这种情况呢

val names = remember { mutableStateListOf("okandgreat", "okandgreat1") }
    val processedNames = remember(names) { names.map { it.uppercase() } }
    Column {
        for (name in processedNames) {
            Text(name, Modifier.clickable {
                names.add("greatandok")
            })
        }
    }

我们发现点击Text时界面并没有发生变化

分析一下为什么不会发生变化

Text onClick => names添加新数据=> recompose => 第一个remember函数不认为list的状态发生了变化,返回原来的names => 因此第二个remrember也不会重新计算

那为什么使用最开始那个例子我们使用string时带参数的remember符合预期呢?

这是因为对srting的改变是值的改变,可以监听到状态的变化,而对list的改变是内容的改变,监听不到状态的变化

当然我们也可以选择将list重新赋值为一个在原来的list基础上添加了新值的list,就符合预期了,但我们使用derivedStateOf更方便

我们就明白DerivedState在什么时候使用了:当remember内的值改变是内容的改变(比如调用add delete函数) 而不是使用=来改变值我们就应该使用DerivedState

val processedNames by remember() { derivedStateOf { names.map { it.uppercase() } } }

这里的remember是保证值没有改变时不会重复计算,而derivedState是为了保证值改变后会重新计算

上面说的是什么时候该用derivedStateOf而不该使用带参数的remember,还有一种情况是该用带参数的remember而不是derivedStateOf

考虑以下代码:

@Composable
fun DerivedState() {
    var useRem by remember { mutableStateOf("UseRemember") }
    UseRemember(useRem) { useRem = "Changed UseRemember" }
}

@Composable
private fun UseRemember(value: String, onClick: () -> Unit) {
    val derivedValue by remember { derivedStateOf { value.uppercase() } }
    val rememberValue = remember(value) { value.uppercase() }

    Text(derivedValue, Modifier.padding(100.dp).clickable { onClick.invoke() })
}

点击Text时,使用带参数的remember可以监听到value的变化变成Changed UseRemember,而derivedStateOf则不可以

这是因为当作为string(int等基本类型都同理)作为函数参数时状态的监听链条被隔断了

分别分析一下俩种情况

derivedStateOf:

onclick=>UseRemember传入新值"Changed UseRemember"=>由于传入的是一个String,所以derivedStateOf无法监听到状态的变化,维持旧值

带参数的remember:

onclick=>UseRemember传入新值"Changed UseRemember"=>remember函数对俩次value进行检测,发现不同,进行重新计算

所以发生错误的原因是因为函数参数是一个基本类型从而隔断了监听链条,我们可以通过使用State函数参数来不完美的解决这个问题

@Composable
fun DerivedState() {
    val useRem = remember { mutableStateOf("UseRemember") }
    UseRemember(useRem) { useRem.value = "Changed UseRemember" }
}

@Composable
private fun UseRemember(value: State<String>, onClick: () -> Unit) {
    val derivedValue by remember { derivedStateOf { value.value.uppercase() } }

    Text(derivedValue, Modifier.padding(100.dp).clickable { onClick.invoke() })
}

为什么说这种方法不完美呢?因为把函数参数缩窄为State限制了这个函数的使用范围,所以我们还是使用带参数的remember更佳

如果函数参数是一个list呢?

@Composable
private fun UseDerive(value: List<String>) {
    val processedValue = remember { derivedStateOf { value.map { it.uppercase() } } }
}

由于mutableStateList也是一个list,所以这里不会发生状态监听链条的隔断

这里也有一点小问题,当函数重新传入的list指向了一个新的对象的时候,会由于remember是无参数的从而不会发生重新计算,因此我们要这样写,将函数参数作为有参数remember的参数

@Composable
private fun UseDerive(value: List<String>) {
    val processedValue = remember(value) { derivedStateOf { value.map { it.uppercase() } } }
}

这里的remember是为了保证value指向的对象改变时可以重新计算,而derivedStateOf是为了保证value发生改变时会重新计算

本节最开始所给出的例子也是这样写的

@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {
    val todoTasks = remember { mutableStateListOf<String>() }
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
    }
}

结论:

1.监听状态变化从而自动刷新,有两种写法:带参数的remember()和不带参数的 remember() + derivedstateOf()

2.当remember内的状态对象的值改变是内容的改变(比如调用add delete函数) 而不是使用=来改变值我们就应该使用DerivedState

3.对于函数参数里的基本类型(String Int 之类),监听链条会被掐断,所以不能用derivedStateOf(),而只能用带参数的remember

但如果是list,监听链条不会被掐断,但注意要使用带参数的remember+derivedStateOf

CompositionLocal

CompositionLocal可以定义具有穿透函数的局部变量

使用方法:

val localName = compositionLocalOf<String> { error("No default value provided") }

@Composable
fun compositionLocal() {
    Column {
        CompositionLocalProvider(localName provides "okandgreat"){
            TextWidget()
        }
    }
    
    TextWidget()
}

@Composable
fun TextWidget() {
    Text(localName.current)
}

使用场景:

提供上下文类型的数据 比如localContext 可以防止变量嵌套过深频繁在函数里定义参数

主题类型的数据

函数参数和CompositionLocal比较

函数参数由函数创作者指定需要传什么参数

而CompositionLocal由自身规定,函数按照CompositionLocal定义去使用

比如localActivity提供当前Activity

localBackground提供当前主题的背景颜色

函数参数和CompositionLocal可以同时使用,注意好优先顺序即可,以Text的颜色属性为例

@Composable
fun Text(
    color: Color = Color.Unspecified,
    style: TextStyle = LocalTextStyle.current
    .....
) {

    val textColor = color.takeOrElse {
        style.color.takeOrElse {
            LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
        }
    }
    val mergedStyle = style.merge(
        TextStyle(
            color = textColor,
            ......
        )
    )
......
}

inline fun Color.takeOrElse(block: () -> Color): Color = if (isSpecified) this else block()

如果特别指定了颜色则选用指定的颜色,如果没有则使用CompositionLocal里的颜色

CompositionLocal的提供是可以嵌套的

val localBackground = compositionLocalOf<Color> { error("No default value provided") }
@Composable
fun compositionLocal() {
    Column {
        CompositionLocalProvider(localBackground provides Color.Green) {
            Widget1()
            CompositionLocalProvider(localBackground provides Color.Cyan) {
                Widget1()
            }
        }

    }
}

@Composable
fun Widget1() {
    Column(Modifier.fillMaxWidth()) {
        Widget2()
    }
}

@Composable
fun Widget2() {
    Button(onClick = {}, Modifier.background(localBackground.current), content = {
        Text("button")
    })
}

staticCompositionLocalOf和compositionLocalOf比较

使用compositionLocalOf情况下会recompose读了该数据的重组作用域,因为此时composer会追踪compositionLocalOf的值的读事件,这个操作会有一定的性能损耗

使用staticCompositionLocalOf情况下会recompose整个区域,这是因为此时composer不会追踪compositionLocalOf的值的读事件

当值通常情况下不会发生改变时推荐使用staticCompositionLocalOf,比如主题颜色,此时性能更好

反之推荐使用compositionLocalOf


标题:Jetpack Compose-状态订阅与自动更新
作者:OkAndGreat
地址:http://zhongtai521.wang/articles/2022/11/21/1669027924625.html

评论
发表评论
       
       
取消