侧边栏壁纸
博主头像
Stars-One的杂货小窝博主等级

所有的创作都是有价值的

  • 累计撰写 254 篇文章
  • 累计创建 46 个标签
  • 累计收到 27 条评论

目 录CONTENT

文章目录

Jetpack Compose学习(8)——State及remeber

Stars-one
2022-07-31 / 0 评论 / 2 点赞 / 167 阅读 / 11488 字
            > 原文地址: [Jetpack Compose学习(8)——State状态及remeber关键字 - Stars-One的杂货小窝](https://stars-one.site/2022/07/30/jetpack-compose-study-8)

之前我们使用TextField,使用到了两个关键字remembermutableStateOf,这两个是做什么用的呢?本篇特来补充说明下

本系列以往文章请查看此分类链接Jetpack compose学习

mutableStateOf

之前也说过,compose是MVVM模式的一种实现,UI界面依赖数据,数据改变即改变UI

这里需要去监听数据,当数据发生改变才会触发UI渲染,改变UI

Android官方将上面这种情况称之为重组,我个人理解觉得重新渲染这个词更好说明

由于数据变化监听逻辑复杂,显然不应该由我们开发者去完成,所以Android官方特地封装好了相应的类供我们使用,便于快速开发,于是就是轮到今天的主角State

从官方的文档说明,State是一个接口,MutableState则是实现了State的一个接口

我们只需要每次创建MutableState对象使用即可,而创建对象的方法Android官方团队也是为我们提供了一个方法,即mutableStateOf()

fun <T : Any?> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T>

本质上,mutableStateOf()方法相当于提供了包装类的功能,里面可包装任意类型的数值,这里为了方便下文讲解,将T称为数值类型

此方法接收两个参数,valuepolicy

  • value 任意数值
  • policy 策略,有三种选择,分别为referentialEqualityPolicy structuralEqualityPolicy neverEqualPolicy

三种的策略说明如下:

  • referentialEqualityPolicy 引用相等策略
  • structuralEqualityPolicy 数值相等策略(默认)
  • neverEqualPolicy 从不相等策略

上面也是说了,当数据发生改变才会触发重组,那么,怎么样才算数据发生改变呢?于是便是有了以上的三种选择

  • 引用相等策略:当数值类型T为对象,State设置新的对象,若是引用相等,则不会触发重组
  • 数值相等策略:当数值类型T为基本数据类型,State设置新数值,若是数值相等,则不会触发重组
  • 从不相等策略:State设置新的对象,不管引用或数值是否相等,一定会会触发重组

个人觉得引用相等和数值相等策略主要是为了优化频繁触发重组带来的性能问题,当然,也提供了一个不优化的选项(从不相等策略)

remember

使用remember和上述的mutableStateOf方法,我们可以创建一个变量,此变量如果是在compose中使用,更改变量数值即可达到更改UI的作用

//mutableState是State<String>对象
val mutableState = remember { mutableStateOf("") } 

//value是String对象
var value by remember { mutableStateOf("") } 

之前一文Kotlin学习快速入门(8)—— 属性委托 - Stars-One的杂货小窝,也是讲解了Kotlin中的委托功能,这里的by就是使用了委托,value的get和set方法委托给了State<String>

所以我们直接对value调用get和set方法,其实都是在调用State<String>对象的get和set方法

将某些状态转为State

在compose中,某些类提供有转为State的方法,方便我们开发时候将其与UI绑定,从而实现动态改变UI的效果

如之前有讲解到的按钮(Button)的按压状态MutableInteractionSource,提供了三个方法来获取不同的State

  • collectIsPressedAsState 按压状态
  • collectIsDraggedAsState 拖动状态
  • collectIsFocusedAsState 焦点状态

除此之外,Compose也是考虑到了之前LiveData组件使用的数据类型,并给予一个扩展方法,可以获得其的State类

需要引用依赖库

implementation androidx.compose.runtime:runtime-livedata:$latestVersion

observeAsState方法 该方法的作用就是将ViewModel提供的LiveData数据转换为Compose需要的State数据

关于LiveData的使用,可以参考这一篇Jetpack架构组件学习(2)——ViewModel和Livedata使用 - Stars-One的杂货小窝

LiveData库中主要是两个类MutableLiveDataLiveData,使用observeAsState即可转为compose中的State对象,如下面例子:

@Composable
fun LoginPageDemo() {

    //这里只是演示用,实际情况MutableLiveData对象是要从ViewModel中取值的!
    val mlivedate = MutableLiveData("myusername")

    //name为State<String>对象,只能读数值,不能修改数值
    var name = mlivedate.observeAsState(initial = "")
    
    Column() {
        val focusManager = LocalFocusManager.current
        TextField(
            modifier = Modifier.fillMaxWidth(),
            value = name.value,
            placeholder = {
                Text("请输入用户名")
            },
            //注意这里,修改数值要去修改MutableLiveData对象
            onValueChange = { str -> mlivedate.value = str}
        )
    }
}

PS:上面的例子比较简单,一般LiveData是与ViewModel联用的,MutableLiveData对象是要从ViewModel中取值的!

除了上面的LiveData,还存在响应式框架RxJava,Flow,也存在对应的转为State的方法,但笔者用的还比较少,所以这方面各位朋友需要自己找些资料了

rememberSaveable

使用remember有个注意点,就是其是对应的生命周期是属于Composable,当Composable被移除的时候(如出现屏幕方向由竖屏转为横屏),remember记住的数值也会丢失

这个时候,推荐使用的则是rememberSaveable

rememberSaveable会在Activity回调configChanges()的时候,将remember的值写入到bundle中,然后重新构建Activity的时候,从bundle读数据

这个关键字就是针对Activity出现重建的情况下进行数值的保存,不会丢失数值,使用方法也是与remember类似

val name = rememberSaveable {
    mutableStateOf("")
}

状态提升

无状态可组合项: 没有状态的可组合项,这意味着它不会保存、定义或修改新状态。

有状态可组合项: 具有可以随时间变化的状态的可组合项。

简单来说,组合中使用 rememberrememberSaveState 方法保存状态的组合项是有状态组合,没有则是无状态组合。

一般如果涉及到组合的复用,则需要我们将组合由有状态组合提取为无状态组合,这就叫做状态提升

当一个组合函数,引入了下面两个参数,即可说此组合其属于有状态组合

  • value: T 形参,即要显示的当前值。
  • onValueChange: (T) -> Unit - 回调 lambda,会在值更改时触发,以便可以在其他位置更新状态(例如,当用户在文本框中输入一些文本时)

那么什么时候需要使用到状态提升呢?主要有下面两个情况:

  • 与多个可组合函数(Composable)共享状态。
  • 创建可在应用中重复使用的无状态可组合项。

下面以之前登录页的输入框为例,有两个输入框,一个是输入用户名,另外一个则是输入密码,但是我们直接是在里面写了两个TextField组件

实际上两个组件十分类似,我们可以采取状态提升将两个组件整成一个自定义组件,这过程则是用到了状态提升

@Composable
fun LoginPageDemo() {

    //这里只是演示用,实际情况MutableLiveData对象是要从ViewModel中取值的!
    val mlivedate = MutableLiveData("myusername")

    //name为State<String>对象,只能读数值,不能修改数值
    var name = mlivedate.observeAsState(initial = "")
    
    Column() {
        InputText(
            imageVector = Icons.Default.AccountBox,
            tip = "请输入用户名",
            value = name,
            onValueChange = { name = it })
        InputText(
            imageVector = Icons.Default.Lock,
            tip = "请输入密码",
            value = pwd,
            onValueChange = { pwd = it },isPwd = true)
    }
}

@Composable
fun InputText(
    imageVector: ImageVector,
    tip: String,
    value: String,
    onValueChange: (String) -> Unit,
    isPwd: Boolean = false
) {

    val pwdVisualTransformation = PasswordVisualTransformation()
    var showPwd by remember {
        mutableStateOf(true)
    }
    val transformation = if (showPwd) pwdVisualTransformation else VisualTransformation.None

    TextField(
        modifier = Modifier.fillMaxWidth(),
        value = value,
        placeholder = {
            Text(tip)
        },
        onValueChange = onValueChange,
        colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),
        leadingIcon = {
            Icon(
                imageVector = imageVector,
                contentDescription = null
            )
        },
        //密码相关配置
        visualTransformation = if (isPwd) transformation else VisualTransformation.None,
        trailingIcon = {
            if (isPwd) {
                if (showPwd) {
                    IconButton(onClick = { showPwd = !showPwd }) {
                        Icon(
                            painter = painterResource(id = R.drawable.eye_hide),
                            contentDescription = null,
                            Modifier.size(30.dp)
                        )
                    }
                } else {
                    IconButton(onClick = { showPwd = !showPwd }) {
                        Icon(
                            painter = painterResource(id = R.drawable.eye_show),
                            contentDescription = null,
                            Modifier.size(30.dp)
                        )
                    }
                }
            }
        },
    )
}

我们不细看代码,从组件的输入参数入手

fun InputText(
    imageVector: ImageVector,
    tip: String,
    value: String,
    onValueChange: (String) -> Unit,
    isPwd: Boolean = false
) {}

其中,value对应的则是TextField的数值,onValueChange则是对应的用户输入事件,我们将此两个封装到参数上,由上层进行设置

PS: 实际上,无状态可组合项是属于比较理想的情形。具体情形中,组合函数里还是会有使用到remember函数进行状态的保存,如上文举出的代码例子

只是我们尽可能让组合项拥有尽可能少的状态,并能够在必要时通过在可组合项的 API 中公开状态来提升状态。

InputText组合函数中,还是存在有remember进行状态的保存,但此函数里的状态不会被上层改变,所以也可以将其视为无组合项(即使与上面说的概念定义有所矛盾)

总结来说:

如果组合函数中存在有value: TonValueChange: (T) -> Unit,我们可以将其视为有状态的可组合项

当然,上面是我自己个人理解,可能不太准确,大家也可以提出意见

一般来说,状态可能还涉及个单一信任源的问题,但笔者还未实践,还是等之后研究到了一起讲解

参考

2

评论区