Android | Compose 状态管理

2022-06-12 13:46:53 浏览数 (1)

前言

应用中的状态指的是可以随时间变化的任何值。这个定义非常广泛,例如从数据库到类的变量,页面上显示的提示信息等。

状态和组合

由于 Compose 是声明式工具集,因此更新它的唯一方法是通新参数调用同一可组合项。这些参数是界面状态表现形式。每当状态更新时,都会发生重组。

可组合项中的状态

可组合函数可以使用 remember 可组合项记住单个对象。系统会在初始组合期间将由 remember 计算的值存储在组合中,并在重组的期间返回存储的值。remember 既可以用于存储可变对象,又可用于存储不可变对象。

注意:remember 会将对象状态存储在组合中,当调用 remember 的可组合项从组合中移除后,它会忘记该对象

mutableStateOf 会创建可观察的 MutableState<T> ,后者是与 Compose 运行时集成的可观察类型。

代码语言:javascript复制
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) }

例子:

代码语言:javascript复制
@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> 的函数:

LiveData
代码语言:javascript复制
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)
                }
            }
        }
    }
}
复制代码
Flow
代码语言:javascript复制
@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)
                }
            }
        }
    }
}
复制代码
RxJava2

可查看

有状态和无状态

使用 remember 存储对象的可组合项会创建内部状态,使该可组合项有状态。例如上面最开始的例子 HomeCompos 就是一个有状态这组合项的示例。因为他在内部会保持和修改 text 状态。在调用方不需要控制状态,并且不必自行管理便可使用状态的情况下,有状态会非常好用,但是有内部状态的组合往往不易重复使用,也更难测试。

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

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

状态提升

Compose 中的状态提升是一种将状态移到可组合项调用方,使得可组合项无状态的模式。Compose 中常规的状态提升模式是将状态变量替换为两个参数:

  • value:T :要显示的当前值
  • onValueChange:(T)->Unit :请求更改值的时间

不过,并不局限于 onValueChange ,如果更具体的事件适合组合项,就可以使用更合适的事件。

以这种方式提升的状态需要一些重要的属性:

  • 单一可信来源:不要进行复制状态,确保只有一个可信来源。这样有助于避免 bug
  • 封装:只有状态可组合项能够修改状态,这完全是内部操作。
  • 可共享:可与多个可组合项共享提升的状态。如果另一个可组合项中执行 name 的操作,可以通过变量提升来做到这一点。
  • 可拦截:无状态可组合项的调用方可以在更改状态之前决定忽略或者修改事件
  • 解耦:无状态的可组合项中提升的状态可以放在任何位置,例如放入 viewModle 中。

下面例子中,使用了状态提升:

代码语言:javascript复制
@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 使用 @Parceize 注解。对象就会变为可打包状态,并且可以捆绑,如下:
代码语言:javascript复制
@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

  • MapSaver 如果有某种原因不能使用 @Parcelize ,就可以使用 mapSaver 定义自己的规则,如下:
代码语言:javascript复制
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"))
    }
}

  • ListSaver 为了避免需要映射定义键,也可以使用 listSaver 并将其索引用作键:
代码语言:javascript复制
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 中可以使用多种不同的方式来管理状态,如:

  • 可组合项:用于管理简单的界面元素状态
  • 状态容器:用于管理复杂页面的元素状态,且用于界面元素的状态和界面逻辑。
  • 架构组件 ViewModel:一种特殊的状态容器类型,用于提供对业务逻辑已经屏幕界面状态的访问权限

下图所示为 compose 状态管理所涉及的各实体之间的关系:

  • 可组合项可以依赖 0个或者多个状态容器,可以使普通对象,或者是viewmodel ,或者二者皆有
  • 如果跑普通的状态容器需要访问业务逻辑或者屏幕状态,则可能需要依赖于 ViewModel
  • ViewModel 依赖于业务层或者数据层

状态和逻辑的类型

在 android 应用中,需要考虑不同的类型状态

  • 界面元素状态:是界面元素的提升状态,例如,ScaffoldState 用于处理 Scaffold 可组合项的状态
  • 屏幕或界面状态:是屏幕需要显示的内容,例如 UserState 类中包含了用户姓名,手机号码等信息。该状态通常会与其他层关联,原因是其包含应用数据。
  • 界面行为逻辑或界面逻辑:与如何在屏幕上显示状态变化相关,例如,导航逻辑决定接下来显示那个屏幕。界面逻辑应始终位于组合中。
  • 业务逻辑:决定如何处理状态变化,例如存储用户偏好设置,该逻辑通常位于数据层,绝对不会位于界面层
将可组合项作为信任来源
代码语言:javascript复制
@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 如下所示:

代码语言:javascript复制
@Stable
class ScaffoldState(
    val drawerState: DrawerState,
    val snackbarHostState: SnackbarHostState
)
复制代码

可以清楚的看到里面保存了两种状态,一个侧边栏的,一个就是提示框。通过 rememberScaffoldState 获取后,就会对状态进行缓存,以防止下次重新组合的时候出现问题。

将状态容器作为可信来源

上面例子中的状态容器 ScaffoldState 是系统提供的,只能保存相对应的状态,如果可组合项包含了多个界面元素状态页面逻辑非常复杂的时候,就应该使用自定义的状态容器了。这样做更容易进行测试,还降低了可组合项的复杂性。

状态容器是在可组合中创建和保存的普通类。状态容器需要遵循 可组合项的生命周期,因此可以此采用 Compose 依赖项。

例如,创建一个状态容器来管理可组合元素的状态,如下:

代码语言:javascript复制
@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 是一种特殊的状态容器类型。其负责:

  • 提供对应用的业务逻辑访问权限,该逻辑通常位于层次结构的其它层,例如业务层和数据层。
  • 准备要在特定的屏幕上呈现的应用数据,这些数据会成为屏幕或者页面的状态。

ViewModel 的生命周比组合长 ,原因是配置发生改变后任然有效,也可以遵守目的地或者导航图的生命周期(Navgiation库)。ViewMode 的生命周期较长,因此不应该保留对绑定到组合生命周期状态的长期引用,否则可能会导致内存泄漏。

一般推荐屏幕级别可组合项来配合 ViewModel 使用。

以下是在屏幕级别的组合项中使用示例:

代码语言:javascript复制
@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 源码浅析

remember

remember 有多个重载,增加了参数 key。 如果 key 等于前一个组合的 key,就返回上一次的值,否则返回一个新值

代码语言:javascript复制
@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
    )
}
复制代码
Composer.cache

获取之前的 value,如果等于之前的值 invalid 就是 false,否则为 true。

代码语言:javascript复制
@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
}
复制代码
Composer.updateRememberedValue

缓存 value,这里最终会调到 updateValue 中,该方法在 Composer 的实现类 ComposerImpl 中:

代码语言:javascript复制
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 {
        ///.....
    }
}

复制代码
代码语言:javascript复制
//保存 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
}
复制代码
Compose.rememberedValue

rememberedValue 最终会调用拿到 nextSlot 中:

代码语言:javascript复制
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 表示状态在数组中的索引。

流程图

总结一下

  • remember 用来记录当前组合项的状态,当重组的时候,可以拿出之前的数据进行使用。但是需要注意的是重组如果是从父组合项开始的,那么状态不会保留。
  • mutableStateOf 创建可观察的 MutableState<T> ,当 value 发生变化后,Compose 就会重组使用 value 的组合项。
  • viewModel 适合在顶级的作用域中使用,例如在 activity 的 最上层可组合函数,以及 navgation 跳转页面中的组合函数中使用。不推荐在普通的组合函数中使用,可能会造成内存泄漏。
  • 管理状态可以分为三种:
    1. 如果状态和逻辑非常简单,就可以使用界面元素状态,例如 ScaffoldState 等。
    2. 自定义状态容器来保存你所需要的状态,然后在通过 remember 进行保存就行了。这种情况适合于屏幕级别组合项的可组合函数,否则当父组合项重组的时候,自己的数据也会丢失。
    3. 使用 Viewmodel 保存状态,ViewModel 的生命周期比组合长,ViewModel 可以遵循 Activity 或者 Fragment 的生命周期,或者是 Navigation 的生命周期。 所以 ViewModel 作用于不是屏幕级别组合项的时候,不应该保留绑定到组合生命周期状态的长期引用,否则可能会导致内存泄漏。
  • 上面的源码浅析有点粗糙,我也只是理解了一下流程,具体的细节也看的有点迷糊,如果有不对的地方还请指正。

参考资料

developer.android.google.cn/jetpack/com…

0 人点赞