zl程序教程

您现在的位置是:首页 >  Java

当前栏目

掌握 Jetpack Compose 中的 State,看这篇就够了

2023-02-18 16:41:00 时间

Jetpack Compose 是响应式 UI 框架。当我们更新 UI 状态时,Compose 会自动刷新 UI,将状态的变化同步到界面上。这个过程是自动的,不需要我们手动调用setTextsetColor之类的方法。

为了实现响应式,Jetpack Compose 使用State对象来感知 UI 状态的变化。

这篇文章会介绍所有和 Compose 的 State (状态) 相关的内容,包括:

  • 什么是状态
  • 如何创建状态
  • 如何使用状态
  • 有状态和无状态可组合项 (composable)

另外,在这篇文章的最后,还附加了额外的内容,不要错过 :-)

Jetpack Compose 中的状态State是什么

在 Jetpack 中,state表示一个和 UI 状态相关的值。每当状态发生改变,Jetpack Compose 都会自动刷新 UI。

State的值可以是任意类型:如像Boolean或者String一样的简单的基础类型,也可以是一个包含整个渲染到屏幕上的 UI 状态的复杂数据类型。

为了让 Compose 能够感知到状态变化,状态的值需要包装到一个State对象里。Jetpack Compose 提供的mutableStateOf()函数就能帮我们完成这个包装操作。这个函数会返回一个MutableState<T>实例,Compose 会跟踪这个实例的变化,在值被修改时进行 UI 更新。

? 不要在 State 实例之外操作状态的值, Compose 会无法感知到对象内容变化,因此也无法更新自动更新 UI 。

data class MyState(var state1: String, var state2: Int)

val myState = MyState("1", 2)

fun MyComposable() {
    val state by remember { mutableStateOf(myState) }
    
    // 无法生效,Compose 感知不到内部字段的变化
    myState.state1 = '2'
    myState.state2 = 3

    // 可以生效,Compose 能感知到 state 本身的变化
    state = MyState('2', 3)
}

Jetpack Compose 里如何构造状态State实例?

创建状态实例的代码如下:

var enabled by remember { mutableStateOf(true) }

可组合项函数中,一般用这行神秘代码来构造状态实例。这行代码乍一看挺让人感到迷惑,让我们来逐词拆解这行代码,看看它做了什么工作:

  • 首先mutableStateOf(true)会返回一个MutableState<Boolean>实例,这个实例中持有了传进去的状态,也就是 true
  • remember {} 函数告诉 Compose,让 Compose 记住传给它的值,这么做可以让 Compose 在每次重新组合 UI 的时候,不会每次都执行传给它的这个 lambda 函数,导致重复执行、覆盖状态。
  • by 是 Kotlin 中用于代理的关键字。它将mutableStateOf()返回的 MutableState实例类型藏了起来,让我们能像操作boolean类型变量一样使用enabled变量。

如果少写了代码行中的几个神秘关键字,会有什么问题吗?

如果不使用mutableStateOf()

@Composablefun MyComponent() {    
    var enabled by remember { true }
    // ...
    Text("Enabled is ${enabled}")
}

? 上面的代码没法正常工作。虽然我们能够去修改enabled变量,但 UI 无法感知到这个变化,也就无法在enabled的变换的时候自动更新。

如果不使用remember {}

@Composablefun MyComponent() {
    var enabled by mutableStateOf(true)
    // ...
    Text("Enabled is ${enabled}")
}

? 同样的这段代码也不能正常工作。当你把enabled改为false,Compose 会在你更新状态的时候刷新 UI 界面。此时它会重新执行mutableStateOf()这段代码,重新创建出一个状态实例,并用一个值为true的enabled变量来渲染界面。

记住这一点(双关):在 Compose 里,我们无法控制我们的 Compose 代码会被多频繁调用,也控制不了它执行的次数。

注意,上面这些讨论只有在 Compose 函数中创建状态的时候成立。如果状态是通过ViewModel创建的,那就不需要使用remember {}对状态进行一层封装。在这种情况下,需要用一些方式来记住这个ViewModel,Compose 提供了viewModel {}hiltViewModel () 函数用来帮我们自动处理这种情况。

如果不使用by关键字?

@Composablefun MyComponent() {
    var enabled = remember { mutableStateOf(true) }
    // ...    
    Text("Enabled is ${enabled.value}")
}

? 这段代码可以正常工作,只是这里的enabled变量会变成MutableState<Boolean>类型。我们不能把它当做Boolean类型进行操作(取值、赋值),要想修改状态,需要像上面的例子那样通过state.value来操作。

不使用by的版本会让代码看起来有点繁琐,但用不用 by 没有限制,看个人喜好选择喜欢的方式就行。

有状态和无状态可组合项

有状态的可组合项是持有自身状态的可组合项。无状态的可组合项是不持有自身状态的可组合项。它们在 Jetpack Compose 里有各自适用的场景。

什么时候应该把可组合项设计成无状态可组合项?

在大多数情况下,我们需要尽可能让可组合项保持无状态。最理想的情况下,整个 UI 界面的状态应该在一个统一地方计算(通常是在ViewModel中),计算完的状态将从上到下传递到所有可组合项里。用这种方式能让开发和测试都变得很简单,不用为了定位问题在多个可组合项里跳来跳去地定位状态变化带来的问题。

一个无状态的可组合项的代码如下:

@Composable
fun MyCustomButton(label: String, onClick: () -> Unit) {
    Button(onClick) {
        Text(label)
    }
}

MyCustomButton可组合项依赖它的调用方传入labelonClick参数。它本身不持有任何状态相关的实例——所以它自然就是一个无状态可组合项。

什么时候应该把组合项设计成有状态组合项?

UI 界面级别的可组合项(也就是负责渲染整个 UI 界面的可组合项)适合设计成持有整个界面状态数据的可组合项。

有状态的可组合项一般会持有ViewModel的引用,由ViewModel负责计算整个 UI 界面的状态。当界面状态发生了改变,新状态会从 UI 界面级别的可组合项一路传递到消费这个状态的子可组合项。

持有ViewModel的 UI 界面的可组合项的代码如下:

@Composable
fun HomeScreen() {
    val homeViewModel = viewModel { HomeScreenViewModel() }

    val state by homeViewModel.inputText
    // TODO use state
}

例外情况:把 TextInput 设计成有状态可组合项

在一些特殊情况下我们可能需要考虑使用一个有状态的可组合项。

举个例子:文本输入和可组合项状态更新之间存在延迟,在快速输入文本的时候你可能会看到诡异的表现,如下面的视频演示的那样。

TODO 转成 Gif

一个简单的规避方式就是把TextInput设计成有状态的可组合项,它将持有需要显示的文本,并通过类似onTextChanged的监听器通知调用方。

@Composable
fun StatefulTextField(
    text: String,
    onTextChanged: (String) -> Unit,
) {
    var state by remember { mutableStateOf(text) }
    TextField(
        value = state,
        onValueChange = {
            state = it
            onTextChanged(it)
        }
    )
}

上面这种处理方法能保证TextField能够在新文本输入的时候第一时间更新,避免实际状态更新带来的延迟问题。

ViewModel中持有状态

把状态放在ViewModel中和把它放在可组合项函数中类似。

使用mutableStateOf()ViewModel中创建表示状态的MutableState<T>实例,在ViewModel内更新 UI 状态,UI 界面能通过这个暴露出来的状态进行 UI 刷新。

class HomeScreenViewModel : ViewModel {

    val inputText by rememberMutableState("")
        private set

    fun onTextChanged(text: String) {
        viewModelScope.launch {
            inputText = text
        }
    }
}

注意,在ViewModel里不需要用到remember {}函数。因为这个函数是一个可组合函数,而可组合函数只能被可组合函数调用,在ViewModel里用不了。在可组合函数中,我们可以用viewModel {}函数,这个函数负责在 Compose 进行重组过程中保证每次返回的都是同一个同一个ViewModel实例。

使可组合项保持无状态的方法:状态提升

顾名思义,状态提升意味着把任何和状态存储相关的状态从可组合项函数中删除,然后通过函数参数将状态的值传进可组合项函数内。

下面是一个有状态的可组合项:

@Composable
fun StatefulCounter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Clicked $count times")
    }
}

进行状态提升改造,将mutableStateOf()的部分删除,然后把状态作为函数参数传进来:

@Composable
fun StatelessCounter(count: Int, onClick : ()->Unit){
    Button(onClick = onClick) {
        Text("Clicked $count times")
    }
}

就这么简单。与其把状态存放在Counter可组合项中,Counter可组合项反过来要求调用者传入count的值用于界面展示和更新。

另外,改造后的Counter可组合项还需要调用者传入监听器,在按钮被点击时把点击事件通知给调用者。

由于StatelessCounter把 UI 逻辑和计数逻辑做了解耦,提升了复用性,进而能够在应用中的不同地方更方便地复用。

通过修改状态更新可组合项目

随着我们越多地使用 Compose 自带的可组合项(如ScaffoldsBottomSheetDrawer等),我们会意识到在 Jetpack Compose 中状态是无处不在的。

下面这个 Bottom Sheet 是一个很好的例子:

val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)

ModalBottomSheetLayout(
    sheetContent = {
        BottomSheetContent()
    },
    sheetState = sheetState
) {
    Button(
        onClick = {
            scope.launch {
                sheetState.show()
            }
    }) {
        Text("Show Sheet")
    }
}

在这个例子里,ModalBottomSheetLayout使用sheetState来修改展示状态,用户点击Button时,点击监听器将收到这个事件,并在处理函数中修改sheetState状态。这是 Jetpack Compose 中很常见的修改状态的模式。

rememberModalBottomSheetState()是一个辅助函数,用来帮我们方便地实现remember { mutableStateOf(ModalBottomSheetState) }这样的代码。

附加内容:在 Jetpack Compose 中,如何使用 Kotlin 的 Flow、RxJava 或者 LiveData 表示状态?

Jetpack Compose 允许我们使用 LiveData、RxJava 的观察者、Kotlin 的 Flow 来表示 Jetpack Compose 中的状态。要做到这点,需要引入相关的拓展方法。这些拓展方法会帮我们把响应式的实例转换成 Jetpack Compose 中的状态实例。

如何在 Jetpack Compose 中使用 Kotlin 的 Flow?

在可组合项函数中用Flow#collectAsStateFlow转为State实例:

val flow = MutableStateFlow("")
// ...
val state by flow.collectAsState()

// for lifecycle aware version
val state by flow.collectAsStateWithLifecycle()

如何在 Jetpack Compose 中使用 LiveData?

首先在模块的依赖里添加 LiveData 拓展的依赖:

dependencies {
    implementation "androidx.compose.runtime:runtime-livedata:x.y.z"
}

然后在代码中用LiveData#observeAsStateLiveData转成State实例:

val liveData = MutableLiveData<String>()
// ...
val state by liveData.observeAsState()

如何在 Jetpack Compose 中使用 RxJava 2 或者 RxJava 3?

首先在模块的依赖里添加 RxJava 拓展的依赖:

dependencies {
    implementation "androidx.compose.runtime:runtime-rxjava2:x.y.z"
    // or implementation "androidx.compose.runtime:runtime-rxjava3:x.y.z"
}

然后在代码中用Observable#subscribeAsStateObservable转为State实例:

val observable = Observable.just("A", "B", "C")

val state by observable.subscribeAsState("initial")

小结

本文介绍了掌握 Jetpack Compose State 所需要了解的相关内容,包括

  • State 在 Jetpeck Compose 中的重要性
  • 如何创建 State 实例
  • 有状态和无状态可组合项的区别
  • 有状态无状态可组合项的使用场景 以及:
  • InputText 的延迟和对应的规避方式
  • 如何在 ViewModel 中表示状态
  • 如何将 Android 中其他表示类型的状态转成 Jetpack Compose 中的状态

希望能对你有帮助。