Jetpack Compose | State状态管理及界面刷新

通过之前文章的介绍,我们知道Jetpack Compose(以下简称Compose)中的 UI 可组合项是通过@Composable 声明的函数来描述的,如:

kotlin 复制代码
@Composable
fun Greeting() {
    Text(
        text = "init",
        color = Color.Red,
        modifier = Modifier.fillMaxWidth()
     )
}

上面的代码描述的是一个静态的 Text,那么如何让 Compose 中的UI更新呢?

状态和重组

Compose 更新UI的唯一方法是通过新参数调用同一可组合项。可组合项中的状态更新时,就会发生重组。

State状态

mutableStateOf() 会创建可观察的 MutableState<T>,如下:

kotlin 复制代码
@Stable
interface MutableState<T> : State<T> {
    override var value: T
}

当value有任何变化时,Compose 会自动为读取 value 的所有可组合函数安排重组。但是靠State只能完成重组,并不能完成UI更新,说的有点绕,直接来看示例:

kotlin 复制代码
@Composable
fun Greeting() {
    val state = mutableStateOf("init")
    log("state:${state.value}")//Logcat
       
    Column {
       Text(
           text = state.value,
           color = Color.Red,
           modifier = Modifier.fillMaxWidth()
        )
       Button(onClick = { state.value = "Jetpack Compose" }) {
          Text(text = "点击更改文本")
        }
     }
}

多次点击按钮,执行结果如下:

kotlin 复制代码
14:25:34.493  E  state:init
14:25:35.919  E  state:init
14:25:37.365  E  state:init
......

可以看到点击Button按钮后确实执行重组了,但是Text中的文本并没有相应更新!这是因为每次进行重组时,可组合项Greeting() 中的 state 又被重新初始化了,导致UI并没有更新。能不能在下次进行重组时保存State<T>中的value值呢,答案是肯定的!可以结合 remember 来使用。

remember

Compose 会在初始组合期间将由 remember 计算的值存储在组合内存中,并在重组期间返回存储的值。remember 既可用于存储可变对象,又可用于存储不可变对象。我们将上面的代码修改如下:

kotlin 复制代码
@Composable
fun Greeting() {
  //前面加了remember,其他都不变
  val state = remember { mutableStateOf("init") }
  log("state:${state.value}")
  ......
}

点击 Button 按钮后:

执行结果:

kotlin 复制代码
15:06:04.544  E  state:init
//点击Button按钮后:
15:06:07.313  E  state:Jetpack Compose

可以看到UI 成功的更新了。

remember(key1 = resId) { } 控制对象缓存的生命周期
kotlin 复制代码
@Composable
inline fun <T> remember(
    key1: Any?,
    calculation: @DisallowComposableCalls () -> T
): T {
    return currentComposer.cache(currentComposer.changed(key1), calculation)
}

除了缓存 State 状态之外,还可以使用 remember 将初始化或计算成本高昂的对象或操作结果存储在组合中。

如上,remember 还可以接受key参数,当key发生变化,缓存值会失效并再次对 lambda 块进行计算。这种机制可控制组合中对象的生命周期。这样带来的好处是不会在每次重组时都进行对象重建高成本操作,如:

kotlin 复制代码
val bitmap = remember(key1 = resId) {
       ShaderBrush(BitmapShader(ImageBitmap.imageResource(res, resId).asAndroidBitmap(),
                  Shader.TileMode.REPEAT, Shader.TileMode.REPEAT
 ))}

上述代码即使发生在频繁重组的可组合项中,只要 key1 = resId 不变,那么ShaderBrush 就不会重新创建,从而提高了性能。

rememberSaveable 与自定义Saver
  • remember 在重组后保持状态,但不会在配置更改后保持状态;
  • 如果想在配置更改后保持状态,可以使用 rememberSaveable 代替;
  • rememberSaveable 会自动保存可保存在 Bundle 中的任何值;如果不支持Bundle存储,可以将对象声明为 @Parcelize 可序列化,如果不能序列化,还可以将其传入自定义 Saver 对象。

示例:

kotlin 复制代码
//1、使用@Parcelize注解
//记得引入 apply plugin: 'kotlin-parcelize'插件
@Parcelize
data class CityParcel(val name: String, val country: String) : Parcelable

data class City(val name: String, val country: String)
//2、MapSaver自定义存储规则,将对象转换为系统可保存到 Bundle 的一组值。
val CityMapSaver = run {
    val nameKey = "Beijing"
    val countryKey = "China"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}
//3、ListSaver自定义存储规则
val CityListSaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

可组合项中使用它们:

kotlin 复制代码
@Composable
fun Greeting() {
// 1、如果涉及到配置更改后的状态恢复,直接使用rememberSaveable,会将值存储到Bundle中
var parcelCity by rememberSaveable {
      mutableStateOf(CityParcel("Beijing", "China"))
}

// 2、如果存储的值不支持Bundle,可以将Model声明为@Parcelable 或者使用MapSaver、ListSaver自定义存储规则
var mapSaverCity by rememberSaveable(stateSaver = CityMapSaver) {
      mutableStateOf(City("Beijing", "China"))
}

var listSaverCity by rememberSaveable(stateSaver = CityListSaver) {
      mutableStateOf(City("Beijing", "China"))
}

log("parcelCity: $parcelCity")
log("mapSaverCity: $mapSaverCity")
log("listSaverCity: $listSaverCity")
}

执行结果:

kotlin 复制代码
17:35:36.810  E  parcelCity: CityParcel(name=Beijing, country=China)
17:35:36.810  E  mapSaverCity: City(name=Beijing, country=China)
17:35:36.810  E  listSaverCity: City(name=Beijing, country=China)

State与 remember结合使用

一般Compose中 MutableState 都是需要跟 remember 组合使用(可乐配鸡翅,天生是一对~),在可组合项中声明 MutableState 对象的方法有三种:

kotlin 复制代码
val mutableState = remember { mutableStateOf("init0") } //1、返回MutableState<T>类型
var value1 by remember { mutableStateOf("init1") } //2、返回T类型
val (value2, setValue) = remember { mutableStateOf("init") } //3、返回两个值分别为:T,Function1<T, kotlin.Unit>

第二种的by委托机制是最常用的,不过需要导入:

kotlin 复制代码
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

UI 接收重组数据的几种方式

现代 Android 架构不管是 MVVM 还是 MVI ,都会用到ViewModel,在ViewModel中通过LiveData、Flow去操作数据,并在UI 层监听数据变化,当数据变化时,UI 层根据监听到的新数据做UI刷新,也就是数据驱动。

这里不再介绍ViewModel、LiveData、Flow的用法,有兴趣的可以参见:ViewModelLiveDataFlow详解

Compose中的 UI 界面刷新思路是一样的,只不过需要将得到的数据进行一下转换而已:

  • 对于 LiveData,需要将 LiveData<T> 转换为 State<T>
  • 对于 Flow,需要将 Flow<T> 转换为 State<T>

记住必须将新数据转换为 State<T>格式,这样 Compose 才可以在状态发生变化后自动重组

Flow.collectAsState() & Flow.collectAsStateWithLifecycle()如何选择

kotlin 复制代码
//ViewModel层
class ComposeVModel : ViewModel(){
    //StateFlow UI层通过该引用观察数据变化
    private val _wanFlow = MutableStateFlow<List<WanModel>>(ArrayList())
    val mWanFlow: StateFlow<List<WanModel>> = _wanFlow

    //请求数据
    fun getWanInfoByFlow(){
        ......
    }
}

//UI层
import androidx.lifecycle.viewmodel.compose.viewModel

@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun Greeting(vm: ComposeVModel = viewModel()) { 
    //2、将 Flow<T> 转换成 State<T>
    val state by vm.mWanFlow.collectAsStateWithLifecycle()
    Column {
       Text(
          text = "$state",
          color = Color.Red,
          modifier = Modifier.fillMaxWidth()
        )
    //1、点击通过ViewModel请求数据
    Button(onClick = { vm.getWanInfoByFlow() }) {
        Text(text = "点击更改文本")
      }
   }
}

上述代码1处通过Button点击进行网络请求,2处负责将 Flow<T> 转换成 State<T>,当数据有更新时,可组合项就可以进行重组,这样整个流程就串起来了。在Android 项目中,collectAsState()collectAsStateWithLifecycle() 该选择哪个使用呢?

1collectAsStateWithLifecycle() 会以生命周期感知型方式从 Flow 收集值。它通过 Compose State 表示最新发出的值,在 Android 开发中请使用这个方法来收集数据流。使用collectAsStateWithLifecycle()必须引入库:

kotlin 复制代码
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha01"

截图来自:developer.android.com/jetpack/and...

2.6.0-alpha01是最低版本,因为我是在AGP7.0以下的项目中使用Compose,如需使用更高版本,自行修改吧~

2collectAsState()collectAsStateWithLifecycle() 类似,但不是生命周期感知的,通常用于跨平台的场景下(Compose也可以跨平台)。collectAsState 可在 compose-runtime 中使用,因此不需要其他依赖项。

LiveData.obseverAsState()

observeAsState() 会开始观察此 LiveData<T>,并在LiveData<T>有数据更新时,自动将其转换为State<T> ,进而触发可组合项的重组。

kotlin 复制代码
//ViewModel层
val mWanLiveData = MutableLiveData<List<WanModel>>()

//UI层
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun Greeting(vm: ComposeVModel = viewModel()) {
   //将 LiveData<T> 转换成 State<T>
   val liveDataState by vm.mWanLiveData.observeAsState()
   ......
}

使用obseverAsState()需要引入:

kotlin 复制代码
implementation "androidx.compose.runtime:runtime-livedata:1.1.1"

注:谷歌建议务必在可组合项中使用 LiveData<T>.observeAsState() 等可组合扩展函数转换类型。

produceState 将对象转换为 State 状态

produceState 会启动一个协程,该协程将作用域限定为可将值推送到返回的 State 的组合。使用此协程将对象转换为 State 状态,例如将外部订阅驱动的状态(如 Flow、LiveData 或 RxJava)引入组合。

即使 produceState 创建了一个协程,它也可用于观察非挂起的数据源。如需移除对该数据源的订阅,请使用 awaitDispose 函数。

看一个官方的示例,展示了如何使用 produceState 从网络加载图像:

kotlin 复制代码
@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {

    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new inputs.
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {

        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

资料

【1】collectAsStateWithLifecycle详解:https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3

【2】State状态:https://developer.android.com/jetpack/compose/state?hl=zh-cn

【3】Compose 中的附带效应:https://developer.android.com/jetpack/compose/side-effects?hl=zh-cn

相关推荐
mmsx3 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
众拾达人6 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌7 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley8 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei10 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576
锋风Fengfeng10 小时前
安卓多渠道apk配置不同签名
android
枫_feng11 小时前
AOSP开发环境配置
android·安卓
叶羽西11 小时前
Android Studio打开一个外部的Android app程序
android·ide·android studio
qq_1715388512 小时前
利用Spring Cloud Gateway Predicate优化微服务路由策略
android·javascript·微服务
Vincent(朱志强)14 小时前
设计模式详解(十二):单例模式——Singleton
android·单例模式·设计模式