Compose 高级附加效应操作符
Compose附加效应
附带效应
是指发生在可组合函数作用域之外的应用状态的变化。由于可组合项的生命周期和属性(例如不可预测的重组、以不同顺序执行可组合项的重组或可以舍弃的重组),可组合项在理想情况下应该是无附带效应的。
如果我们的可组合函数里面出现了附带效应的情况,就会导致附带效应在不恰当的时机出现
如:
@Composable
fun MyScreen(
title:String
){
Log.d("UI日志","MyScreen")
Column{
Text(title)
}
}
这样写的结果是每当MyScreen刷新的时候,都会输出一遍日志
可以看出如果在Compose组合函数中如果出现了附带效应可能带来严重的错误,下面是一些高级(相比较于remember和mutableState而言)的解决Compose中附带效应的方法
我们通常会在请求网络数据,开启协程,处理回调时使用附加效应
SideEffect
SideEffect无论重组多少次都只会执行一次
@Composable
fun MyComposable(state: MyState) {
SideEffect {
// Make a network request to get the latest data.
val data = fetchData()
// Update the state with the latest data.
state.data = data
}
// Display the UI that depends on the state.
}
SideEffect 仅执行一次,因此 fetchData() 函数仅被调用一次
LauchedEffect
不可以在Compose作用域中直接使用协程,这会导致协程被启动很多次,需使用LauchedEffect
@Composable
fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
LaunchedEffect(Unit) {
delay(SplashWaitTime) // Simulates loading things
onTimeout()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}
rememberUpdatedState
如果是在协程里delay了一段时间再去执行一个回调,有可能由于发生了重组回调已经不是最新的了,因此我们需要使用rememberUpdatedState去保证延迟到达时执行的回调是最新的
@Composable
fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// This will always refer to the latest onTimeout function that
// LandingScreen was recomposed with
val currentOnTimeout by rememberUpdatedState(onTimeout)
// Create an effect that matches the lifecycle of LandingScreen.
// If LandingScreen recomposes or onTimeout changes,
// the delay shouldn't start again.
LaunchedEffect(Unit) {
delay(SplashWaitTime)
currentOnTimeout()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}
rememberCoroutineScope
有一些诸如点击回调的地方,我们也希望启动协程任务,这样LaunchedEffect就无法满足我们的需求了,因为LaunchedEffect是一个可组合函数,他无法在重组作用域以外的地方调用。
此外,LaunchedEffect不和Compose的作用域绑定,因此Compose 函数从Compose的继承树移除后LaunchedEffect发起的协程也还是在运行,这种情况我们也可以使用rememberCoroutineScope去解决
@Composable
fun CraneHome(
onExploreItemClicked: OnExploreItemClicked,
modifier: Modifier = Modifier,
) {
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
modifier = Modifier.statusBarsPadding(),
drawerContent = {
CraneDrawer()
}
) {
val scope = rememberCoroutineScope()
CraneHomeContent(
modifier = modifier,
onExploreItemClicked = onExploreItemClicked,
openDrawer = {
scope.launch {
scaffoldState.drawerState.open()
}
}
)
}
}
fun MyComposable(state: MyState) {
val scope = rememberCoroutineScope()
scope.launch {
// Fetch data from the network.
val data = fetchData()
// Update the state with the data.
state.data = data
}
// Display the UI that depends on the state.
}
DisposableEffect
当前可组合函数去订阅某些信息,而且可组合函数销毁的时候取消订阅
假设我们有一个这样的天气服务,可以通知所有的订阅者当前的天气。
interface WeatherListener{
fun onUpdate(weather:String)
}
object WeatherService{
private val observerList=mutableListOf<WeatherListener>()
fun addObserver(observer:WeatherListener)= observerList.add(observer)
fun remove(observer: WeatherListener)=observerList.remove(observer)
fun update(){
observerList.forEach {
it.onUpdate("下雨了")
}
}
}
我们希望在一个组合中订阅实时的天气,可以这样做:
@Composable
fun Weather(){
var weatherString by remember{ mutableStateOf("") }
DisposableEffect(Unit){
val listener=object:WeatherListener{
override fun onUpdate(weather: String) {
weatherString=weather
}
}
WeatherService.addObserver(listener)
onDispose {
WeatherService.remove(listener)
}
}
Text("当前的天气:${weatherString}")
}
ProduceState
ProduceState是remember和LaunchedEffect
的结合体
//Composable with remember and LaunchedEffect
fun MyComposable(value: Int) {
val state = remember { mutableStateOf("") }
// Update the state variable with the user's name.
LaunchedEffect(key1 = "keyName") {
state.value = fetchUserName()
}
// Display the user's name.
Text(text = state.value)
}
//Composable with produceState
fun MyComposableWithProduceState(value: Int) {
val state = produceState(initialValue = "") {
// Update the state variable with the user's name.
value = fetchUserName()
}
// Display the user's name.
Text(text = state.value)
}
假设我们需要展示一个界面,包含等待请求数据态,请求成功态和请求失败态,我们可以定义一个Sealed Class UiState,然后在view model里通过flow来发送这些状态,UI层再根据这些状态做出不同的展示
在Compose里,我们可以使用produceState来实现以上的效果
data class DetailsUiState(
val cityDetails: ExploreModel? = null,
val isLoading: Boolean = false,
val throwError: Boolean = false
)
@Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
Box(modifier.fillMaxSize()) {
CircularProgressIndicator(
color = MaterialTheme.colors.onSurface,
modifier = Modifier.align(Alignment.Center)
)
}
}
else -> { onErrorLoading() }
}
}
derivedStateOf
derivedStateOf可以监听旧的state值,当它发生改成时去进行改变自己的值
因此derivedStateOf可以用来format数据,计算衍生的数据
fun MyComposable(value: Int) {
// Create a state variable that stores the user's name.
val nameState = remember { mutableStateOf("") }
// Create a new state variable that stores the user's name in uppercase.
val nameInUpperCaseState = remember { derivedStateOf {
// Convert the user's name to uppercase.
return it.value.uppercase()
}
}
// Display the user's name in uppercase.
Text(text = nameInUpperCaseState.value)
}
那么什么时候应该使用derivedStateOf呢?
答案是:当希望更新UI的次数小于key改变的次数时,应该使用derivedStateOf
derivedStateOf有点类似于Kotlin Flow里的distinctUntilChanged
var username by remember { mutableStateOf("") }
val submitEnabled = isUsernameValid(username)
看到这个例子
每当用户输入一次就会去改变一次submitEnabled的状态,即使后面都是true了,这显然是耗费性能的
这个时候我们就可以使用derivedStateOf
var username by remember { mutableStateOf("") }
val submitEnabled = remember {
derivedStateOf { isUsernameValid(username) }
}
一些问题:
derivedStateOf需要被remember吗?
是的,derivedStateOf和mutableStateOf差不多,如果想在重组过程中存活的话,就需要使用remember
derivedStateOf还有什么典型应用场景
比如只有在用户滑动一个LazyColumn后才able一个按钮
val isEnabled = lazyListState.firstVisibileItemIndex > 0
此时我们就可以使用
val isEnabled = remember {
derivedStateOf { lazyListState.firstVisibleItemIndex > 0 }
}
什么使用remember(key)和derivedStateOf需要一起使用
derivedStateOf只能监听Compose state object,其他的值就不可以,如果我们还需要监听Compose state object之外的值来更改状态的话,就需要使用remember(key)
@Composable
fun ScrollToTopButton(lazyListState: LazyListState, threshold: Int) {
// There is a bug here
val isEnabled by remember {
derivedStateOf { lazyListState.firstVisibleItemIndex > threshold }
}
Button(onClick = { }, enabled = isEnabled) {
Text("Scroll to top")
}
}
在这段代码中,其实是有错误的,因为threshold有可能更改
因此我们需要这样修改
val isEnabled by remember(threshold) {
derivedStateOf { lazyListState.firstVisibleItemIndex > threshold }
}
标题:Compose 高级附加效应操作符
作者:OkAndGreat
地址:http://zhongtai521.wang/articles/2023/04/03/1691545300736.html