Compose 高级附加效应操作符

2023-04-03/2023-08-23

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)

img

img

看到这个例子

每当用户输入一次就会去改变一次submitEnabled的状态,即使后面都是true了,这显然是耗费性能的

这个时候我们就可以使用derivedStateOf

var username by remember { mutableStateOf("") }
val submitEnabled = remember {
  derivedStateOf { isUsernameValid(username) }
}

img

img

一些问题:

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

评论
发表评论
       
       
取消