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

相关推荐
每次的天空27 分钟前
Android学习总结之应用启动流程(从点击图标到界面显示)
android·学习
一清三白2 小时前
Android Studio 连接雷电模拟器教程
android
姜行运2 小时前
数据结构【栈和队列附顺序表应用算法】
android·c语言·数据结构·算法
wang_peng3 小时前
android studio 基础
android·ide·android studio
〆、风神5 小时前
EasyExcel 数据字典转换器实战:注解驱动设计
android·java·注解
stevenzqzq5 小时前
Android studio xml布局预览中 Automotive和Autotive Distant Display的区别
android·xml·android studio
QING6186 小时前
Kotlin commonPrefixWith用法及代码示例
android·kotlin·源码阅读
QING6186 小时前
Kotlin groupByTo用法及代码示例
android·kotlin·源码阅读
兰琛11 小时前
Compose组件转换XML布局
android·xml·kotlin
水w13 小时前
【Android Studio】解决报错问题Algorithm HmacPBESHA256 not available
android·开发语言·android studio