应用中的状态指的是可以随时间变化的任何值。这个定义非常广泛,例如从数据库到类的变量,页面上显示的提示信息等。
由于 Compose 是声明式工具集,因此更新它的唯一方法是通新参数调用同一可组合项。这些参数是界面状态表现形式。每当状态更新时,都会发生重组。
可组合函数可以使用 remember
可组合项记住单个对象。系统会在初始组合期间将由 remember
计算的值存储在组合中,并在重组的期间返回存储的值。remember
既可以用于存储可变对象,又可用于存储不可变对象。
注意:remember 会将对象状态存储在组合中,当调用 remember 的可组合项从组合中移除后,它会忘记该对象
mutableStateOf
会创建可观察的 MutableState<T>
,后者是与 Compose 运行时集成的可观察类型。
interface MutableState<T> : State<T> {
override var value: T
}
复制代码
value 如果有任何更改,系统会重新读取 value 的所有可组合函数。对于 ExpandingCard
,每当 expanded 发生变化时,都会导致重组 EXPANDINGcARD
。
在可组合项中声明 MutableState
对象的方法有三种:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
例子:
@Composable
fun HomeCompos() {
var text by remember { mutableStateOf("") }
SetScaffold(title = "首页") {
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Top) {
Button(onClick = {
text = "hello word"
}) {
if (text.isEmpty()) {
Text(text = "hello", fontSize = 50.sp)
} else {
Text(text = text, fontSize = 50.sp)
}
}
}
}
}
复制代码
虽然 remember
可以帮助在组合后保持状态,但不会帮助在配置更改后保持状态。为此,你必须使用 rememberSaveable
来保存配置改变后的状态,例如屏幕旋转。
Jetpack Compose 并不要求必须使用 MutableState<T>
存储状态。事实上也支持其他的类型,但是在 Compsoe 读取其他可观察类型之前,需要将其转为 State ,以便 Compose 可以在状态发生改变的时候进行重组。
Compose 附带一下可以根据 Android 应用中常见的观察类型创建 State<T>
的函数:
fun HomeCompos(navController: NavHostController) {
var text by remember { mutableStateOf("") }
val liveData = MutableLiveData("").observeAsState()
val liveData = MutableLiveData("")
val liveDataState = liveData.observeAsState()
SetScaffold(title = "首页") {
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Top) {
Button(onClick = {
liveData.value = "Hello 345"
}) {
if (liveDataState.value!!.isEmpty()) {
Text(text = "hello", fontSize = 50.sp)
} else {
Text(text = liveDataState.value!!, fontSize = 50.sp)
}
}
}
}
}
复制代码
@Composable
fun HomeCompos(navController: NavHostController) {
val flow = flow<String> {
emit("Hello ")
delay(2000)
emit("Hello 345")
}
val flowState = flow.collectAsState("345")
SetScaffold(title = "首页") {
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Top) {
Button(onClick = {}) {
if (flowState.value.isEmpty()) {
Text(text = "hello", fontSize = 50.sp)
} else {
Text(text = flowState.value, fontSize = 50.sp)
}
}
}
}
}
复制代码
使用 remember
存储对象的可组合项会创建内部状态,使该可组合项有状态。例如上面最开始的例子 HomeCompos
就是一个有状态这组合项的示例。因为他在内部会保持和修改 text
状态。在调用方不需要控制状态,并且不必自行管理便可使用状态的情况下,有状态
会非常好用,但是有内部状态的组合往往不易重复使用,也更难测试。
无状态可组合项是指不保持任何状态的可组合项。实现的一种简单的方式是使用 状态提升
。
在开发可重复使用组合项时,你通常需要同时提供一组有状态的版本和无状态的版本。有状态版本对于不关心状态来说很方便,而无状态版本对于都需要控制或提升状态的调用来说是必要的。
Compose 中的状态提升是一种将状态移到可组合项调用方,使得可组合项无状态的模式。Compose 中常规的状态提升模式是将状态变量替换为两个参数:
value:T
:要显示的当前值onValueChange:(T)->Unit
:请求更改值的时间不过,并不局限于 onValueChange
,如果更具体的事件适合组合项,就可以使用更合适的事件。
以这种方式提升的状态需要一些重要的属性:
下面例子中,使用了状态提升:
@Composable
fun HomeCompose() {
var text by remember { mutableStateOf("") }
Log.e("---345--->", "11111");
Content(text = text, onTextChange = { text = it })
}
@Composable
fun Content(text: String, onTextChange: (String) -> Unit) {
SetScaffold(title = "首页") {
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Top) {
Button(onClick = {
onTextChange.invoke("Hello 345")
}) {
if (text.isEmpty()) {
Text(text = "hello", fontSize = 50.sp)
} else {
Text(text = text, fontSize = 50.sp)
}
}
}
}
}
复制代码
通过从 Content 中提升出状态,更容易推断该组合项,在不同的情况下使用它,以及进行测试。
HelloContent 与状态的存储方式解耦,这意味着如果你更换或者修改 HomeCompose ,不必修改 Content 的实现方式。
状态下降,时间上升 这种模式简称为 单向数据流
。这种情况下,状态会从 HomeCompose 下降到 Content 中,事件会从 Content 中上升到 HomeCompose 中。通过遵守单向数据流,我们可以将页面中显示状态的可组合项与应用中存储和更改的部分解耦。
注意:提升状态时,有三条规则可以帮助你弄清楚状态应去向何处 1,状态应至少提升到使用该状态(读取)的所有可组合项的最低共同父项。 2,状态应至少提升到他可以发生变化(写入)的最高级别 3,如果两种状态发生变化以响应相同的事件,他们应一起提升
在 activity 重新创建后,可以使用 rememberSaveable
恢复界面状态。rememberSaveable
可以在重组后保持状态,此外,也可以在重新创建 activity 和进程后保持状态
添加到 Bundle
的所有数据类型都会被保存。如果要保存无法添加到 Bundle
的内容,您有以下集中选择
@Parcelize
data class City(val name: String, val country: String) : Parcelable
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
@Parcelize
,就可以使用 mapSaver 定义自己的规则,如下:
data class City(val name: String, val country: String)
val CitySaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(
save = { mapOf(nameKey to it.name, countryKey to it.country) },
restore = { City(it[nameKey] as String, it[countryKey] as String) }
)
}
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
data class City(val name: String, val country: String)
val CitySaver = listSaver<City, Any>(
save = { listOf(it.name, it.country) },
restore = { City(it[0] as String, it[1] as String) }
)
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
你可以通过组合函数本身管理简单的状态提升。但是随着状态数量的增加,或者组合函数中出现要执行的逻辑,最好将逻辑和状态事务委托给其他类(状态容器)。
状态容器用于管理可组合项的逻辑和状态,状态容器也被称为 "提升的状态对象"
状态容器的大小不等,具体取决于所管理界面元素的范围(从底部应用栏等单个微件到整个屏幕)。状态容器可以组合使用,也就是说,可以将某个状态容器集成到其他状态容器中,尤其是在汇总状态时。
Compose 中可以使用多种不同的方式来管理状态,如:
下图所示为 compose 状态管理所涉及的各实体之间的关系:
在 android 应用中,需要考虑不同的类型状态
ScaffoldState
用于处理 Scaffold
可组合项的状态UserState
类中包含了用户姓名,手机号码等信息。该状态通常会与其他层关联,原因是其包含应用数据。@Composable
fun Content() {
val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
Button(onClick = {
coroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar("hello")
}
}) {
Text(text = "按钮")
}
}
}
复制代码
scaffoldState
中包含了 Scaffold
的可变属性,例如侧边栏的状态,以及显示提示框等,ScaffoldState
如下所示:
@Stable
class ScaffoldState(
val drawerState: DrawerState,
val snackbarHostState: SnackbarHostState
)
复制代码
可以清楚的看到里面保存了两种状态,一个侧边栏的,一个就是提示框。通过 rememberScaffoldState
获取后,就会对状态进行缓存,以防止下次重新组合的时候出现问题。
上面例子中的状态容器 ScaffoldState
是系统提供的,只能保存相对应的状态,如果可组合项包含了多个界面元素状态页面逻辑非常复杂的时候,就应该使用自定义的状态容器了。这样做更容易进行测试,还降低了可组合项的复杂性。
状态容器是在可组合中创建和保存的普通类。状态容器需要遵循 可组合项的生命周期
,因此可以此采用 Compose 依赖项。
例如,创建一个状态容器来管理可组合元素的状态,如下:
@Composable
fun HomeCompose() {
val userState = rememberUserState()
Log.e("---345--->", "${userState.value.name} ${userState.value.age}");
Scaffold() {
Column() {
Spacer(modifier = Modifier.padding(top = 50.dp))
Button(onClick = {
userState.value = MyUserState("张三", 26)
}) {
Text(text = "按钮")
}
Spacer(modifier = Modifier.padding(top = 100.dp))
Text(text = "姓名 ${userState.value.name}")
Text(text = "姓名 ${userState.value.age}")
}
}
}
@Composable
fun rememberUserState(myUserState: MutableState<MyUserState> = mutableStateOf(MyUserState())) =
remember() {
mutableStateOf(MyUserState(myUserState.value.name, myUserState.value.age))
}
class MyUserState(
var name: String = "345",
var age: Int = 20
) {
var sex: String = "男"
fun getUserId(): Int {
return 0
}
}
复制代码
上面代码中创建了一个状态容器 MyUserState
用来保存用户信息,然后通过 remember
对状态进行保存,当 HomeCompose 重组的时候,就可以获取到之前的数据,例如在 onClick
中修改了 value 的值,这就会导致 HomeCompose 重组,但是获取到的值是已经保存的值了。
需要注意的是 remember
有个参数 key,这个参数默认不需要传入,key 的作用是用来判断重复性的,例如重组的时候这个key和上一次的可以不相同,那么就不会获取之前保存的值,而是直接创建一个新的状态。
还有一点须要注意:rember 的恢复只限于当前函数的重组,例如只有 HomeCompose
进行重组,状态才可以恢复。如果是 HomeCompose
父作用域进行重组,那么状态也是会没有的。
如果普通的状态容器类负责界面逻辑以及界面元素的状态,则 ViewModel
是一种特殊的状态容器类型。其负责:
ViewModel 的生命周比组合长 ,原因是配置发生改变后任然有效,也可以遵守目的地或者导航图的生命周期(Navgiation库)。ViewMode 的生命周期较长,因此不应该保留对绑定到组合生命周期状态的长期引用,否则可能会导致内存泄漏。
一般推荐屏幕级别可组合项来配合 ViewModel 使用。
以下是在屏幕级别的组合项中使用示例:
@Composable
fun HomeDetail(
viewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
) {
Log.e("---345--->", viewModel.state.name);
Log.e("---345--->", viewModel.toString());
Scaffold() {
Column() {
Spacer(modifier = Modifier.padding(top = 50.dp))
Button(onClick = {
viewModel.state = MyUserState("张三", 50)
}) {
Text(text = "按钮")
}
Spacer(modifier = Modifier.padding(top = 100.dp))
Text(text = "姓名 ${viewModel.state.name}")
Text(text = "姓名 ${viewModel.state.age}")
}
}
}
class HomeViewModel : ViewModel() {
var state by mutableStateOf(MyUserState())
}
class MyUserState(
var name: String = "345",
var age: Int = 20
) {
var sex: String = "男"
fun getUserId(): Int {
return 0
}
}
复制代码
上面的 viewmodel 就是在屏幕级别的组合项中使用,HomeDetail
该组合项是通过 navigation
跳转过去的,所以当退出该页面的时候 viewmodel 会被释放。每次进入都会创建新的 viewModel。
另外,如果 ViewModle 在非顶级的组合中使用时,即使该组合以及父组合重建,该 ViewMode 也不重建,因为 VIewModel 的生命周期大于可组合项,所以这种情况 ViewModel 尽可能的不要依赖可组合项,否则可能会出现内存泄漏。
remember 有多个重载,增加了参数 key
。 如果 key 等于前一个组合的 key,就返回上一次的值,否则返回一个新值
@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
/**
* 如果key1等于前一个组合,则记住计算返回的值,否则通过调用计算生成并记住一个新值
*/
@Composable
inline fun <T> remember(
key1: Any?,
calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(currentComposer.changed(key1), calculation)
}
/**
* 如果 [key1] 和 [key2] 等于之前的组合,则记住 [calculation] 返回的值,否则通过调用 [calculation] 生成并记住一个新值。
*/
@Composable
inline fun <T> remember(
key1: Any?,
key2: Any?,
calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(
currentComposer.changed(key1) or currentComposer.changed(key2),
calculation
)
}
复制代码
获取之前的 value,如果等于之前的值 invalid
就是 false,否则为 true。
@ComposeCompilerApi
inline fun <T> Composer.cache(invalid: Boolean, block: () -> T): T {
@Suppress("UNCHECKED_CAST")
return rememberedValue().let {
//如果 key 不等于之前的值 或者之前没有缓存
if (invalid || it === Composer.Empty) {
// 缓存 value ,并返回
val value = block()
updateRememberedValue(value)
value
} else it //返回之前缓存的 value
} as T
}
复制代码
缓存 value,这里最终会调到 updateValue
中,该方法在 Composer
的实现类 ComposerImpl
中:
internal fun updateValue(value: Any?) {
//如果组合当前正在调度要插入树的节点,则为真
if (inserting) {
//将value保存,最终会存在 slots 数组中
writer.update(value)
//
if (value is RememberObserver) {
//将 value 添加到更改列表
record { _, _, rememberManager -> rememberManager.remembering(value) }
abandonSet.add(value)
}
} else {
///.....
}
}
复制代码
//保存 value
fun update(value: Any?): Any? {
val result = skip()
set(value)
return result
}
fun skip(): Any? {
if (insertCount > 0) {
insertSlots(1, parent)
}
return slots[dataIndexToDataAddress(currentSlot++)]
}
fun set(value: Any?) {
runtimeCheck(currentSlot <= currentSlotEnd) {
"Writing to an invalid slot"
}
slots[dataIndexToDataAddress(currentSlot - 1)] = value
}
复制代码
rememberedValue 最终会调用拿到 nextSlot 中:
internal fun nextSlot(): Any? = if (inserting) { //如果正在将新的节点插入到视图数中 ,染回 Emepty
validateNodeNotExpected()
Composer.Empty
} else reader.next().let { //获取 value
//如果重用,返回 Empty,否则 返回 value
if (reusing) Composer.Empty else it
}
fun next(): Any? {
if (emptyCount > 0 || currentSlot >= currentSlotEnd) return Composer.Empty
return slots[currentSlot++]
}
复制代码
slots 是一个树组,currentSlot 表示状态在数组中的索引。
MutableState<T>
,当 value 发生变化后,Compose 就会重组使用 value 的组合项。
ScaffoldState
等。
remember
进行保存就行了。这种情况适合于屏幕级别组合项的可组合函数,否则当父组合项重组的时候,自己的数据也会丢失。