我们知道Jetpack Compose(以下简称Compose)
中的 UI 可组合项是通过@Composable
声明的函数来描述的,如:
@Composable
fun Greeting() {
Text(
text = "init",
color = Color.Red,
modifier = Modifier.fillMaxWidth()
)
}
上面的代码描述的是一个静态的 Text
,那么如何让 Compose
中的UI
更新呢?
状态和重组
Compose 更新UI的唯一方法是通过新参数调用同一可组合项。可组合项中的状态更新时,就会发生重组。
State状态
mutableStateOf()
会创建可观察的 MutableState<T>
,如下:
@Stable
interface MutableState<T> : State<T> {
override var value: T
}
当value有任何变化时,Compose 会自动为读取 value 的所有可组合函数安排重组。但是靠State只能完成重组,并不能完成UI更新,说的有点绕,直接来看示例:
@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 = "点击更改文本")
}
}
}
多次点击按钮,执行结果如下:
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 既可用于存储可变对象,又可用于存储不可变对象。我们将上面的代码修改如下:
@Composable
fun Greeting() {
//前面加了remember,其他都不变
val state = remember { mutableStateOf("init") }
log("state:${state.value}")
......
}
点击 Button 按钮后:
执行结果:
15:06:04.544 E state:init
//点击Button按钮后:
15:06:07.313 E state:Jetpack Compose
可以看到UI 成功的更新了。
remember(key1 = resId) { } 控制对象缓存的生命周期
@Composable
inline fun <T> remember(
key1: Any?,
calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(currentComposer.changed(key1), calculation)
}
除了缓存 State 状态之外,还可以使用 remember 将初始化或计算成本高昂的对象或操作结果存储在组合中。
如上,remember
还可以接受key参数,当key发生变化,缓存值会失效并再次对 lambda 块进行计算。这种机制可控制组合中对象的生命周期。这样带来的好处是不会在每次重组时都进行对象重建高成本操作,如:
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 对象。
示例:
//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) }
)
可组合项中使用它们:
@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")
}
执行结果:
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 对象的方法有三种:
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委托机制是最常用的,不过需要导入:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
UI 接收重组数据的几种方式
现代 Android 架构不管是 MVVM 还是 MVI ,都会用到ViewModel,在ViewModel中通过LiveData、Flow去操作数据,并在UI 层监听数据变化,当数据变化时,UI 层根据监听到的新数据做UI刷新,也就是数据驱动。
Compose中的 UI 界面刷新思路是一样的,只不过需要将得到的数据进行一下转换而已:
- 对于
LiveData
,需要将LiveData<T>
转换为State<T>
; - 对于
Flow
,需要将Flow<T>
转换为State<T>
。
记住必须将新数据转换为 State<T>
格式,这样 Compose 才可以在状态发生变化后自动重组。
Flow.collectAsState() & Flow.collectAsStateWithLifecycle()如何选择
//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()
该选择哪个使用呢?
1、collectAsStateWithLifecycle()
会以生命周期感知型方式从 Flow 收集值。它通过 Compose State 表示最新发出的值,在 Android 开发中请使用这个方法来收集数据流。使用collectAsStateWithLifecycle()
必须引入库:
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha01"
2.6.0-alpha01
是最低版本,因为我是在AGP7.0以下的项目中使用Compose,如需使用更高版本,自行修改吧~
2、 collectAsState()
与 collectAsStateWithLifecycle()
类似,但不是生命周期感知的,通常用于跨平台的场景下(Compose也可以跨平台)。collectAsState
可在 compose-runtime
中使用,因此不需要其他依赖项。
LiveData.obseverAsState()
observeAsState() 会开始观察此 LiveData<T>
,并在LiveData<T>
有数据更新时,自动将其转换为State<T>
,进而触发可组合项的重组。
//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()需要引入:
implementation "androidx.compose.runtime:runtime-livedata:1.1.1"
注:谷歌建议务必在可组合项中使用 LiveData<T>.observeAsState()
等可组合扩展函数转换类型。
produceState 将对象转换为 State 状态
produceState 会启动一个协程,该协程将作用域限定为可将值推送到返回的 State 的组合。使用此协程将对象转换为 State 状态,例如将外部订阅驱动的状态(如 Flow、LiveData 或 RxJava)引入组合。
即使 produceState 创建了一个协程,它也可用于观察非挂起的数据源。如需移除对该数据源的订阅,请使用 awaitDispose 函数。
看一个官方的示例,展示了如何使用 produceState 从网络加载图像:
@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)
}
}
}
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap