Compose编程思想 -- 深入理解声明式UI的状态订阅与自动更新

相关文章:

Compose编程思想 -- 初识Compose

前言

在上一篇文章中,主要介绍了Compose中常用的组件使用方式,以及Compose设计的一些思想,其中提到了Compose作为一种声明式UI,与传统View的区别,本文将会深入讲解声明式UI的状态订阅与自动更新,看声明式UI如何处理界面的刷新逻辑。

1 自定义Composable

首先我们先看下Compose当中一个非常重要的注解@Composable,如果要在页面中展示控件,那么需要定义@Composable函数,这是规定。

kotlin 复制代码
class ComposeActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            testModifier()
        }
    }

    @Composable
    fun testModifier() {

        Column(
            modifier = Modifier
                .background(Color.Blue)
        ) {
            Image(
                painter = painterResource(id = R.drawable.ic_launcher_background),
                contentDescription = null,
                modifier = Modifier
                    .padding(12.dp)
                    .width(100.dp)
                    .height(80.dp)
            )

            Spacer(modifier = Modifier.size(10.dp))

            Text(
                text = "测试Modifier",
                fontSize = 20.sp,
                modifier = Modifier.background(Color.Red)
                    .clickable {

                    }
                    .padding(12.dp),
                fontWeight = FontWeight(400),
                color = Color.White
            )
        }

    }
    
}

例如setContent函数,这是ComponentActivity的一个扩展函数,第二个参数content明确要求需要一个@Composable函数,用来声明要展示的UI内容。

kotlin 复制代码
* @param parent The parent composition reference to coordinate scheduling of composition updates
 * @param content A `@Composable` function declaring the UI contents
 */
public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
){
    // ......
}

而且我们看到的所有的组件,基本都是通过Composable注解修饰,原因就是像Box,Column,Row,LazyColumn等容器型的组件,他们需要传入的content也都必须是一个Composable函数。

kotlin 复制代码
@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
) {
    // ......
}

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

那么既然这是一个规定了,那么我们遵守就好了。从工程维度来看,既然使用到了注解,那么必然会有注解处理器来做一些额外的处理,不然使用注解的意义在哪呢?

首先我们先思考一下,Java中对于注解的处理方式大概分为两种:

  • APT技术,即注解处理工具(Annatation Processor Tools),需要自定义注解处理器,来对jar包中的代码做编译时的处理;
  • 字节码插桩,常见的就是ASM、JavaPoet、KotlinPoet、JavaAssist等,在我们的代码中额外插入一些逻辑代码。

从整个工程来看,没有任何Compose的注解处理器的定义,那就说明@Composable的注解处理与平台无关,而是使用了编译器插件完成的。为什么这么做?是因为Compose要作为一个多平台的框架,像APT和字节码插桩其实只是处理.class字节码,属于JVM维度的修改,如果在桌面版或者WEB就不再适用了。

因此,对于@Composable注解的处理,不同的编译器插件(Compiler Plugin)生成的代码不一样,例如Android平台会生成.class文件,而WEB平台可能会生成js代码(有精力的伙伴可以看下,欢迎随时打脸)。

1.1 使用场景

什么情况下会使用到自定义@Composable,这不免会让我们联想到自定义View。传统的自定义View,通常会通过xml定义布局范式,通过自定义View类来实现逻辑细节。

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_image"
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:src="@drawable/ic_launcher_background" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textColor="#DF1010"
        android:textSize="20dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/iv_image"
        tools:text="文案展示" />

</androidx.constraintlayout.widget.ConstraintLayout>

具体的自定义类实现:

kotlin 复制代码
class ImageText(
    context: Context,
    attributeSet: AttributeSet? = null
) : ConstraintLayout(context, attributeSet, 0) {

    private var binding: LayoutTextImageBinding

    init {
        binding = LayoutTextImageBinding.bind(
            LayoutInflater.from(context).inflate(R.layout.layout_text_image, this, true)
        )
    }

    fun setText(content: String) {
        binding.tvContent.text = content
    }

    fun setImage(id: Int) {
        binding.ivImage.setImageResource(id)
    }

    fun setImage(url: String) {
        //TODO 
    }
}

而如果使用Composable实现这个效果则是非常的简洁,不需要xml + 自定义类结合,只需要一个@Composable函数即可。

kotlin 复制代码
@Composable
fun ImageText(content: String) {
    val rememberContent = remember(key1 = content) { mutableStateOf(content) }
    Column (
        horizontalAlignment = Alignment.CenterHorizontally
    ){
        Image(
            painter = painterResource(id = R.drawable.ic_launcher_background),
            contentDescription = null,
            modifier = Modifier.size(200.dp)
        )
        // 这里起到的就是  android:layout_marginTop="10dp" 的作用
        Spacer(modifier = Modifier.height(10.dp))
        Text(
            text = rememberContent.value,
            fontSize = 20.sp,
            color = Color.Red,
        )
    }
}

2 State状态订阅

从本小节开始,我会介绍Compose作为声明式UI框架,如何实现状态订阅和自动刷新。

2.1 MutableState

在Compose中,如果想要实现自动刷新,那么需要感知数据的变化并更新,因此会使用MutableState来保存数据,从官方的注释中不难看出:

MutableState是用来持有数据,并在Composable函数执行的过程中可以从value属性中读出对应的值。而且在Compose组建的过程中,这个值的变化会被订阅,当value值发生变化时,所有订阅这个值的组件会发生重组,如果之前订阅的值没有发生改变,那么就不会发生重组。

kotlin 复制代码
/**
 * A mutable value holder where reads to the [value] property during the execution of a [Composable]
 * function, the current [RecomposeScope] will be subscribed to changes of that value. When the
 * [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
 * will be scheduled. If [value] is written to with the same value, no recompositions will be
 * scheduled.
 *
 * @see [State]
 * @see [mutableStateOf]
 */
@Stable
interface MutableState<T> : State<T> {
    override var value: T
    operator fun component1(): T
    operator fun component2(): (T) -> Unit
}

在官方的解释中,有两个关键词「订阅 subscribe」和「重组 recomposition」,首先我们先看下,为什么使用MutableState存储的值可以被订阅,这也是自动刷新的原理所在。

2.1.1 MutableState的订阅

kotlin 复制代码
val state = mutableStateOf("展示文案")

setContent {
    // state被订阅了,当state存储的值变化了,会recomposition
    Text(text = state.value)
}
lifecycleScope.launch {
    delay(3_000)
    state.value = "刷新了"
}

看下源码,这个订阅关系如何实现的?

kotlin 复制代码
fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)

其实调用mutableStateOf最终返回的就是一个MutableState对象,类似于mutableListof这种快捷创建。

通过createSnapshotMutableState方法创建,最终会返回一个SnapshotMutableStateImpl对象,其实现了StateObject接口,前面我们说到MutableState能够被订阅,其实真正被订阅的是StateObject接口.

kotlin 复制代码
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
        
    // ...... 
}

我们看下StateObject接口:

  • firstStateRecord参数是一个链表结构,用来记录所有值的变化过程;

  • prependStateRecord方法,是当值发生变化时,会将新的变化的值记录在链表的头部,采用头插法。

  • mergeRecords方法,是用来处理冲突的。

kotlin 复制代码
@JvmDefaultWithCompatibility
interface StateObject {
    /**
     * The first state record in a linked list of state records.
     */
    val firstStateRecord: StateRecord

    /**
     * Add a new state record to the beginning of a list. After this call [firstStateRecord] should
     * be [value].
     */
    fun prependStateRecord(value: StateRecord)

    /**
     * Produce a merged state based on the conflicting state changes.
     *
     * This method must not modify any of the records received and should treat the state records
     * as immutable, even the [applied] record.
     *
     * @param previous the state record that was used to create the [applied] record and is a state
     * that also (though indirectly) produced the [current] record.
     *
     * @param current the state record of the parent snapshot or global state.
     *
     * @param applied the state record that is being applied of the parent snapshot or global
     * state.
     *
     * @return the modified state or `null` if the values cannot be merged. If the states cannot
     * be merged the current apply will fail. Any of the parameters can be returned as a result.
     * If it is not one of the parameter values then it *must* be a new value that is created by
     * calling [StateRecord.create] on one of the records passed and then can be modified
     * to have the merged value before being returned. If a new record is returned
     * [MutableSnapshot.apply] will update the internal snapshot id and call
     * [prependStateRecord] if the record is used.
     */
    fun mergeRecords(
        previous: StateRecord,
        current: StateRecord,
        applied: StateRecord
    ): StateRecord? = null
}

所以,当创建SnapshotMutableStateImpl对象时,会创建一个StateRecord对象,并给firstStateRecord赋值。

kotlin 复制代码
fun <T : StateRecord> T.readable(state: StateObject): T {
    val snapshot = Snapshot.current
    // 订阅当前快照
    // invoke the observer associated the current snapshot
    snapshot.readObserver?.invoke(state)
    return readable(this, snapshot.id, snapshot.invalid) ?: sync {
        val syncSnapshot = Snapshot.current
        @Suppress("UNCHECKED_CAST")
        readable(state.firstStateRecord as T, syncSnapshot.id, syncSnapshot.invalid) ?: readError()
    }
}

从源码中可以看到,当获取value值的时候,首先会让观察者订阅这个快照,观察者可以认为就是Compose UI;然后从firstStateRecord中取到最新的值(从链表头部)。

kotlin 复制代码
@Suppress("UNCHECKED_CAST")
override var value: T
    // ......
    set(value) = next.withCurrent {
        // 比较StateRecord中的值,与设置的新值是否一致
        if (!policy.equivalent(it.value, value)) {
            next.overwritable(this, it) { this.value = value }
        }
    }

value的值发生变化时,则是会调用set方法,会将当前值与StateRecord中的值比较是否一致,如果不一致,那么就会调用overwritable函数,在这个函数中,会调用notifyWrite函数,这个函数就是用来通知订阅者,进行重组。何谓重组,其实就是重新执行@Composable方法。

kotlin 复制代码
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)
    }
}

2.1.2 自动刷新中的坑

如此看来,Compose作为声明式UI框架,是采用了观察者的设计模式来进行订阅和发布。

kotlin 复制代码
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
    snapshot.writeObserver?.invoke(state)
}
kotlin 复制代码
fun <T : StateRecord> T.readable(state: StateObject): T {
    val snapshot = Snapshot.current
    snapshot.readObserver?.invoke(state)
    // ......
}

如前面我们的分析,当对MutableState进行订阅的时候,其实是对StateObject的订阅,官方一点的说明就是SnapShot中的读写观察者对StateObject的订阅,分别通过readObserver和writeObserver进行读和写的订阅。

这里需要注意一点的是:只有在组合的过程中,进行读写的操作,才会被监听到,才能自动刷新。

什么意思?看个场景:

kotlin 复制代码
setContent {
    Box(modifier = Modifier
        .clickable {
            state.value = "刷新了22222"
        }
        .size(200.dp)
        .background(Color.Red)
    ) {
        // 先获取值,readObserver订阅了
        Text(text = state.value, modifier = Modifier.align(Alignment.Center))
        // 修改值,
        state.value = "刷新了"
    }
}

因为state的值是在组合的时候发生了更新,那么Text在之前订阅的这块区域其实就被标记为失效,等到下一帧刷新的时候,就会刷新到最新的值。

如果我们设置了单击事件,那么这个过程其实是在组合之后的,其实已经组合完成了,那么再做值的修改,其实并不能被Snapshot中的writeObserver监听到,从而无法完成界面的自动刷新。

前面我们介绍的是在组合阶段的订阅,会发生重组行为,完成界面的刷新;而重组之后进行值的修改,那么就涉及到了新值的应用行为, 这将会是另一套监听机制,后续会介绍。

2.2 remember

既然MutableState的值发生变化,会导致重组,我们看下下面的例子,Text展示初始值为Alex,当3s后刷新state的值,此时我们看现象就是:Text展示的值没有发生变化。

kotlin 复制代码
@SuppressLint("UnrememberedMutableState")
@Composable
fun RememberUpdate() {
    Log.d("TAG", "RememberUpdate: composition")
    val state = mutableStateOf("Alex")
    Text(text = state.value)
    handler.postDelayed({
        state.value = "time wait"
    }, 3_000)
}

其实不难理解,因为state值的变化,导致@Composable函数重新执行,即发生重组,此时state被重新初始化为Alex,那么Text展示的文案依然是Alex。

那么有什么方案能够解决这个问题?

  • state从成员变量改为全局变量,从@Composable中移除;
  • state通过remember修饰。
kotlin 复制代码
@SuppressLint("UnrememberedMutableState")
@Composable
fun RememberUpdate() {
    Log.d("TAG", "RememberUpdate: composition")
    val state = remember { mutableStateOf("Alex") }

    Text(text = state.value)
    handler.postDelayed({
        state.value = "time wait"
    }, 3_000)
}

通过remember包装mutableStateOf之后,我们发现Text的文案正常刷新了。那么问题来了,remember干了什么事帮助我们解决这个问题。

kotlin 复制代码
/**
 * Remember the value produced by [calculation]. [calculation] will only be evaluated during the composition.
 * Recomposition will always return the value produced by composition.
 */
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

看下remember的官方解释:

remember只会在组合的过程中将值计算下来并保存,重组的时候将会一直返回组合阶段存储的值

可以这么理解,只有在初始化的时候,会执行block内部的代码 ,即初始化MutableState,后续将不会重复初始化,而是会取缓存中的值。

kotlin 复制代码
@ComposeCompilerApi
inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T {
    @Suppress("UNCHECKED_CAST")
    return rememberedValue().let {
        if (invalid || it === Composer.Empty) {
            val value = block()
            updateRememberedValue(value)
            value
        } else it
    } as T
}

从源码中可以看出,remember类似于一个单例,只有当内存中的值为Composer.Empty时,也就是初始化的时候,会执行初始化的操作并更新内存中remember,后续的执行都会从缓存中取,remember是有返回值的,返回的就是block中的数据类型。

2.3 remember的"遗忘"

remember从词义上看,是记住的意思,但是并不代表remember会记住所有的状态,看一下下面的计数器。

kotlin 复制代码
@Composable
fun CountTick() {

    //记录数值
    var count by remember { mutableStateOf(0) }

    Column {

        Text(text = "当前数值 $count")

        if (count > 10) {
            var showNotice by remember { mutableStateOf(true) }
            if (showNotice) {
                Row {
                    Text(text = "你已经超过10次了,加油!")
                    Spacer(modifier = Modifier.width(20.dp))
                    Text(text = "清除", modifier = Modifier.clickable {
                        showNotice = false
                    })
                }
            }
        }

        Button(onClick = {
            count++
        }) {
            Text(text = "计数++")
        }

        Button(onClick = { count = 0 }) {
            Text(text = "RESET")
        }

    }
}

点击按钮1,会使得count累加,会发生重组;当count累加超过10之后,会显示一行文案,此时可以选择点击「清除」,将这行文案清除。

当点击清除之后,会将showNotice的值置反,并触发重组,此时count > 10,会进到if代码块中,因为showNotice值为false,因此不会展示文案,并不需要像传统view那样,将view的visibility置为GONE。

当点击「RESET」按钮时,会将count置为0,此时触发重组,当count值超过10之后,发生奇怪的现象,之前已经被清除的文案,又再次显示出来了,我们记得之前已经将showNotice置为了false。

所以这里就涉及到了remember的一个特性:当发生重组的时候,如果没有再次执行之前remember的来源位置,那么状态就会丢失。

我们回顾一下,当count > 10之后,进入到if代码块,此时showNotice的值为true,展示了文案;点击清除将showNotice状态置为false,触发重组之后,因为count > 10,因此依然会进到if代码块并再次执行了showNotice的位置,此时为false,就没有展示。

但是点击「RESET」之后,count = 0,那么if代码块不再进入,此时并没有在重组的时候,再次调用remember的来源未知,导致状态丢失,此后count > 10之后,文案再次展示出来。

如果要解决这个问题,可以将状态存储到ViewModel中。

kotlin 复制代码
class MainViewModel : ViewModel() {


    private val _showNotice = mutableStateOf(true)

    var showNotice: Boolean
        get() = _showNotice.value
        private set(value) {}

    fun backup() {
        _showNotice.value = false
    }

}
kotlin 复制代码
@Composable
    fun CountTick() {

        //记录数值
        var count by remember { mutableStateOf(0) }

        Column {

            Text(text = "当前数值 $count")

            if (count > 10) {
//                var showNotice by remember { mutableStateOf(true) }
                if (viewModel.showNotice) {
                    Row {
                        Text(text = "你已经超过10次了,加油!")
                        Spacer(modifier = Modifier.width(20.dp))
                        Text(text = "清除", modifier = Modifier.clickable {
                            viewModel.backup()
                        })
                    }
                }
            }

            Button(onClick = {
                count++
            }) {
                Text(text = "计数++")
            }

            Button(onClick = { count = 0 }) {
                Text(text = "RESET")
            }

        }
    }

其实使用ViewModel的目的就是做全局状态的存储,而且避开remember的特性。这也提醒了我们,在Compose开发中,对于数据的存储要严格存放在ViewModel中,从而避免出现非常规的bug。

2.4 Column刷新

前面我们介绍了当使用mutableStateOf保证数据的时候,当读数据的时候,会设置观察者;在写数据的时候,重组作用域内调用该数据的部分失效,然后下一帧到来时会触发刷新,更新为最新的数据。

看下下面的例子,当我们点击Button按钮的时候,我们将列表数据新增一个元素,看是否会触发刷新。

kotlin 复制代码
@Composable
fun ListView() {

    val datas by remember {
        mutableStateOf(mutableListOf("1", "2", "3"))
    }

    Column {
        for (data in datas) {
            Text(text = "第 $data Item")
        }
        Button(onClick = { 
            datas.add("4") 
        }) {
            Text(text = "添加元素")
        }
    }

}

经过测试之后,我们发现点击按钮,列表数据没有刷新。我们的数据明明发生了变化,为什么没有触发刷新呢?

kotlin 复制代码
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 }
            }
        }

我们再回顾一下源码,我们看到只有在调用setValue方法的时候,才会触发Snapshot # writeObserver的刷新监听,我们在Button按钮中并没有发生赋值操作,而仅仅是操作了元素新增一个成员。

所以要想实现刷新,一个方案就是采用赋值的方式,每次都创建一个新的List给老的List赋值。

kotlin 复制代码
@Composable
fun ListView() {

    var datas by remember {
        mutableStateOf(mutableListOf("1", "2", "3"))
    }

    Column {
        for (data in datas) {
            Text(text = "第 $data Item")
        }
        Button(onClick = {
            datas = datas.toMutableList().apply {
                add("4")
            }
        }) {
            Text(text = "添加元素")
        }
    }

}

但这种方式并不优雅,因此Compose中提供了一个可以被订阅的List容器mutableStateListOf,从源码中可以看到,其继承自StateObject,具备订阅的能力。

kotlin 复制代码
fun <T> mutableStateListOf(vararg elements: T) =
    SnapshotStateList<T>().also { it.addAll(elements.toList()) }

@Stable
class SnapshotStateList<T> : MutableList<T>, StateObject

那么使用mutableStateListOf来装载列表数据,就非常合理了,而且符合我们的开发思路。

kotlin 复制代码
@Composable
fun ListView() {

    var datas = remember {
        mutableStateListOf("1","2","3")
    }

    Column {
        for (data in datas) {
            Text(text = "第 $data Item")
        }
        Button(onClick = {
            datas.add("4")
        }) {
            Text(text = "添加元素")
        }
    }

}

除此之外,还有mutableStateMapOf用于监控key-value数据的变化。

3 Compose状态提升

「状态」这个概念,其实就是控件(组件)的属性,例如传统View中的TextView,我们可以通过getTextsetText给TextView设置文案或者获取文案,这种android:text属于TextView的属性,也可以称之为状态,那么在Compose中,我们能拿到Text的内容吗?显然拿不到,Text并没有提供这个方法,因此我们称之为「无状态」。

当然这也是从组件单一维度来看是无状态的,看下下面的场景:

kotlin 复制代码
@Composable
fun TextComponent() {
    var name = "Stateless"
    Text(name)
    Button(onClick = { name = "Stateful" }) {
        Text(text = "点击")
    }
}

我们知道TextButton是无状态的,无法获取自身的信息,但是对于TextComponent组合函数来说,它是有状态的,它可以拿到Text展示的文案数据,所以在Compose中,并不是所有的成员都是无状态的。

3.1 状态提升

对于TextComponent来说,它是有状态的,那么如何在外部拿到Text显示的文案数据,那么有效的手段就是状态提升,将有状态变为无状态。

kotlin 复制代码
@Composable
fun TextComponent() {
    var name = "Stateless"
    Text(name)
}

无状态指的有内部状态,但是没有外部状态,因此我们可以通过将状态向外提,从而达到状态提升的效果。

kotlin 复制代码
@Composable
fun TextComponent(name: String) {
    Text(name)
}
kotlin 复制代码
val name by remember {
    mutableStateOf("Alex")
}
TextComponent(name = name)

通过在@Composable函数中添加参数,将内部状态对外暴露,从而能获取TextComponent的内部状态,那么TextComponent就变成无状态的组件,因为无法获取内部组件的信息。

总结一下:有状态组件,就是将状态(属性)关在了组件内部,无法共享出去。想要共享出去,就需要通过状态提升的方式,通过外部传值的方式,将有状态组件变为无状态组件,当然状态能不共享就不共享,减少出错的概率。

相关推荐
西瓜本瓜@2 小时前
在Android中fragment的生命周期
android·开发语言·android studio·kt
老哥不老4 小时前
MySQL安装教程
android·mysql·adb
xcLeigh5 小时前
html实现好看的多种风格手风琴折叠菜单效果合集(附源码)
android·java·html
图王大胜6 小时前
Android SystemUI组件(07)锁屏KeyguardViewMediator分析
android·framework·systemui·锁屏
InsightAndroid6 小时前
Android通知服务及相关概念
android
aqi008 小时前
FFmpeg开发笔记(五十四)使用EasyPusher实现移动端的RTSP直播
android·ffmpeg·音视频·直播·流媒体
Leoysq8 小时前
Unity实现原始的发射子弹效果
android
起司锅仔8 小时前
ActivityManagerService Activity的启动流程(2)
android·安卓
猿小蔡8 小时前
Android Bitmap 和Drawable的区别
android
峥嵘life8 小时前
Android14 手机蓝牙配对后阻塞问题解决
android·智能手机