Jetpack Compose 是响应式 UI 框架。当我们更新 UI 状态时,Compose 会自动刷新 UI,将状态的变化同步到界面上。这个过程是自动的,不需要我们手动调用setText
或setColor
之类的方法。
为了实现响应式,Jetpack Compose 使用State
对象来感知 UI 状态的变化。
这篇文章会介绍所有和 Compose 的 State (状态) 相关的内容,包括:
另外,在这篇文章的最后,还附加了额外的内容,不要错过 :-)
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)
}
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,C
ompose 会在你更新状态的时候刷新 UI 界面。此时它会重新执行mutableStateOf()这段
代码,重新创建出一个状态实例,并用一个值为true的e
nabled变量
来渲染界面。
记住这一点(双关):在 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
可组合项依赖它的调用方传入label
和onClick
参数。它本身不持有任何状态相关的实例——所以它自然就是一个无状态可组合项。
UI 界面级别的可组合项(也就是负责渲染整个 UI 界面的可组合项)适合设计成持有整个界面状态数据的可组合项。
有状态的可组合项一般会持有ViewModel
的引用,由ViewModel
负责计算整个 UI 界面的状态。当界面状态发生了改变,新状态会从 UI 界面级别的可组合项一路传递到消费这个状态的子可组合项。
持有ViewModel
的 UI 界面的可组合项的代码如下:
@Composable
fun HomeScreen() {
val homeViewModel = viewModel { HomeScreenViewModel() }
val state by homeViewModel.inputText
// TODO use state
}
在一些特殊情况下我们可能需要考虑使用一个有状态的可组合项。
举个例子:文本输入和可组合项状态更新之间存在延迟,在快速输入文本的时候你可能会看到诡异的表现,如下面的视频演示的那样。
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 自带的可组合项(如Scaffolds
、BottomSheet
、Drawer
等),我们会意识到在 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 允许我们使用 LiveData、RxJava 的观察者、Kotlin 的 Flow 来表示 Jetpack Compose 中的状态。要做到这点,需要引入相关的拓展方法。这些拓展方法会帮我们把响应式的实例转换成 Jetpack Compose 中的状态实例。
在可组合项函数中用Flow#collectAsState
将Flow
转为State
实例:
val flow = MutableStateFlow("")
// ...
val state by flow.collectAsState()
// for lifecycle aware version
val state by flow.collectAsStateWithLifecycle()
首先在模块的依赖里添加 LiveData 拓展的依赖:
dependencies {
implementation "androidx.compose.runtime:runtime-livedata:x.y.z"
}
然后在代码中用LiveData#observeAsState
将LiveData
转成State
实例:
val liveData = MutableLiveData<String>()
// ...
val state by liveData.observeAsState()
首先在模块的依赖里添加 RxJava 拓展的依赖:
dependencies {
implementation "androidx.compose.runtime:runtime-rxjava2:x.y.z"
// or implementation "androidx.compose.runtime:runtime-rxjava3:x.y.z"
}
然后在代码中用Observable#subscribeAsState
将Observable
转为State
实例:
val observable = Observable.just("A", "B", "C")
val state by observable.subscribeAsState("initial")
本文介绍了掌握 Jetpack Compose State 所需要了解的相关内容,包括
希望能对你有帮助。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。