Jetpack Compose中的附带效应及效应处理器

Jetpack Compose中的附带效应及效应处理器


将在任何可组合函数范围之外运行的代码称为附带效应。

为什么要编写在任何可组合函数范围之外的代码?

这是因为可组合项的生命周期和属性(例如不可预测的重组)会执行可组合项的重组。

让我们通过一个示例来理解为什么我们需要在Compose项目中使用附带效应。

@Composable
fun WithoutSideEffectExample() {
    
    

    var counter by remember {
    
     mutableStateOf(0) }
    val context = LocalContext.current

    // on every recomposition , this toast will show
    context.showToast("Hello")

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
    
    
        Button(onClick = {
    
     counter++ }) {
    
    
            Text(text = "Click here")
        }
        SpacerHeight()
        Text(text = "$counter")
    }

}

就像你可以在上面的例子中看到的那样,我编写的代码没有使用附带效应。这段代码有一个按钮和文本,在每次点击按钮时,计数器变量会增加并显示在Text的组合函数中。当我们首次启动应用程序时,我们还会显示一个弹出消息。

当我们首次启动应用程序时,它运行得很好,但当我们点击按钮时,计数器变量会增加,并且弹出消息会再次显示。再次点击按钮,弹出消息又会显示。请参考下面的GIF。

未使用附带效应

为什么?我们都知道,当状态改变时(这里是计数变量改变,因为它是一个可变状态),会发生重新组合(这个组合函数会重新构建)。

在这种情况下,我们必须将我们的提示代码写在附带效应内部,这样它只会运行一次,并且我们可以对该代码进行控制。

注意:永远不要在组合函数内部运行任何non-composable代码,总是使用side-effect来处理。

为了处理这些附带效应,我们有各种effect-handlersside-effect states

有两种类型的Effect-Handlers

Suspended effect handler

effect-handler用于挂起函数

  • LaunchEffect
  • rememberCoroutineScope

Non-suspended effect handler

此effect-handler用于非挂起函数

  • DisposableEffect
  • SideEffect

有四种Side-Effect States

  • rememberUpdateState
  • produceState
  • derivedStateOf
  • snapShotFlow
    让我们逐个了解所有这些

LaunchEffect

LaunchEffect 是一个可组合函数,用于在组件启动时执行副作用。它接受两个参数:key 和 coroutineScope 块。

  • key 参数中,您可以传递任何状态,因为它的类型是 Any。
  • coroutineScope 块中,您可以传递任何暂停或非暂停函数。
  • LaunchEffect 会在可组合函数中始终运行一次。

如果您希望再次运行 LaunchEffect 块,则必须在key参数中传递任何随时间变化的状态(mutableStateOf,StateFlow)。
有很多理论,让我们通过一个示例来理解

示例-1

让我们通过 LaunchEffect 解决上述的 toast 问题

@Composable
fun WithLaunchEffect() {
    
    

    var counter by remember {
    
     mutableStateOf(0) }
    val context = LocalContext.current


    LaunchedEffect(key1 = true) {
    
    
        context.showToast("Hello")
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
    
    
        Button(onClick = {
    
     counter++ }) {
    
    
            Text(text = "Click here")
        }
        SpacerHeight()
        Text(text = "$counter")
    }

}

正如您在上面的代码中所看到的,我们将showToast代码移到了launch effect中。这意味着当您首次启动应用程序时,launch effect块将在可组合函数中被调用一次。

现在Toast只显示一次,在点击按钮后对toast代码没有影响。
假设你的toast代码在启动效果中,而你希望在点击按钮时显示toast信息。让我们看看如何做到这一点?
这是一个非常简单的事情,只需将counter变量传递到key参数中即可。

@Composable
fun WithLaunchEffect() {
    
    

    var counter by remember {
    
     mutableStateOf(0) }
    val context = LocalContext.current


    LaunchedEffect(key1 = counter) {
    
    
        context.showToast("Hello")
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
    
    
        Button(onClick = {
    
     counter++ }) {
    
    
            Text(text = "Click here")
        }
        SpacerHeight()
        Text(text = "$counter")
    }

}

counter变量改变时,启动launch effect并显示提示消息。

示例-2

让我们看另一个包含 API 调用的示例。

sealed class ApiState<out T> {
    
    
    data class Success<T>(val data: String) : ApiState<T>()
    object Loading : ApiState<Nothing>()
    object Empty : ApiState<Nothing>()
}

class MainViewModel : ViewModel() {
    
    
    private val _apiState: MutableState<ApiState<String>> = mutableStateOf(ApiState.Empty)
    var apiState: State<ApiState<String>> = _apiState
        private set

    fun getApiData() = viewModelScope.launch {
    
    
        _apiState.value = ApiState.Loading
        delay(2000)
        _apiState.value = ApiState.Success("Data loaded successfully..")
    }

}

@Composable
fun LaunchEffectExample() {
    
    
    val viewModel: MainViewModel = viewModel()
    var call by remember {
    
     mutableStateOf(false) }

    LaunchedEffect(key1 = call) {
    
    
        viewModel.getApiData()
    }
// never call this function here as whenever recomposition occurs this function will call again
//    viewModel.getApiData()

    when (val res = viewModel.apiState.value) {
    
    
        is ApiState.Success -> {
    
    
            Log.d("main", "Success")
            Column(
                modifier = Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
    
    
                Text(text = res.data, fontSize = 25.sp)
                SpacerHeight()
                Button(onClick = {
    
    
                    call = !call
                }) {
    
    
                    Text(text = "Call Api again !")
                }
            }
        }
        ApiState.Loading -> {
    
    
            Log.d("main", "Loading")
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    
    
                CircularProgressIndicator()
            }
        }
        ApiState.Empty -> {
    
    }
    }

}

如您在上述代码中所见,我们正在进行伪 API 调用,从 viewModel 中获取成功和失败状态。正如您在组合函数中注意到的那样,我们在启动效果中传递了 getApiData 函数。如果我们不这样做,我们的 API 将会一次又一次地调用,因为我们的 viewModel.apiState 值发生变化时,我们的组合函数将会重组,然后再次调用 getApiData 函数,因此这个循环将会继续下去,永远不会结束。

记住我上面说的,永远不要在composable函数内运行任何non-composable的代码,始终使用附带效应来实现。

如果你想再次调用这个API,我们可以使用可变状态变量,将其传递给键参数,每当这个变量的值改变时,getApiData函数将被调用。

rememberCoroutineScope()

这是Jetpack Compose中的一个可组合函数,它将创建与当前组合相关联的协程作用域,我们可以在其中调用任何挂起函数。

  • 此协程作用域可用于启动新的协程,当组合(可组合函数)不再活动时,这些协程将自动取消。
  • rememberCoroutineScope()创建的CoroutineScope对象对于每个组合而言是单例的。这意味着如果在同一组合中多次调用该函数,它将返回相同的协程作用域对象。

让我们通过一个示例来理解,你知道如何在Jetpack Compose中创建Snackbar吗?不知道?让我们构建它。

@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun CoroutineScopeExample() {
    
    
    val state = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    Scaffold(
        scaffoldState = state,
    ) {
    
    
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    
    
            Button(onClick = {
    
    
                scope.launch {
    
    
                    state.snackbarHostState.showSnackbar("Hey How are you?")
                }
            }) {
    
    
                Text(text = "Show Snackbar")
            }
        }
    }

}

在上面的代码中,当我们点击按钮时,它将显示一个 Snackbar

scope.launch {
    
    
    state.snackbarHostState.showSnackbar("Hey How are you?")
}

这里的.showSnackbar是一个挂起函数,意味着我们必须在协程作用域中调用它,因此我们创建了一个rememberCoroutineScope协议函数。

scope.cancel()

如果上述协程作用域在多个地方使用,并且在编写此代码时使用scope.cancel(),所有协程作用域将被取消。

val job =  scope.launch {
    
    
       state.snackbarHostState.showSnackbar("Hey How are you?")
   }
   job.cancel()

如果您想取消当前作用域,只需编写上述代码即可!

DisposableEffect

DisposableEffect 是 Jetpack Compose 的一个函数,允许您创建需要在 Composable 首次渲染或销毁时执行的副作用。

该函数接受两个参数,第一个参数是需要执行的副作用,第二个参数是触发副作用运行的依赖项列表。

让我们通过一个例子来理解,基本上在按钮点击时,我们会发送一个广播事件来检查设备是否处于飞行模式,并看看我们如何在这里使用disposable effect

@Composable
fun AirplaneModeScreen() {
    
    

    var data by remember{
    
     mutableStateOf("No State") }
    val context = LocalContext.current
    val broadcastReceiver = remember {
    
    
        object : BroadcastReceiver(){
    
    
            override fun onReceive(context: Context?, intent: Intent?) {
    
    
                val bundle = intent?.getBooleanExtra("state",false) ?: return
                data = if(bundle)
                    "Airplane mode enabled"
                else
                    "Airplane mode disabled"
            }

        }
    }

    DisposableEffect(key1 = true){
    
    
        val intentFilter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
        context.applicationContext.registerReceiver(broadcastReceiver,intentFilter)
        onDispose {
    
    
            context.applicationContext.unregisterReceiver(broadcastReceiver)
        }
    }

    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center){
    
    
        Text(text = data)
    }

}

如您在上面的代码中所见,我们正在发送一个广播事件来检查飞行模式。

但是,当您关注DisposableEffect的代码时,它看起来很奇怪。因此,在这个disposable effect函数中,我们注册了广播接收器,并且在其中还包含一个名为onDispose的函数,用于在不再使用时(当这个可组合函数销毁时)对广播接收器进行释放或取消注册。

注意:每当您遇到需要在不再使用时释放或取消注册某个内容的情况时,不要犹豫,只需使用DisposableEffect。希望您明白了!

SideEffect

SideEffect是Jetpack Compose中的一个函数,用于在不影响UI性能的情况下进行side work

让我们通过一个示例来了解

不使用SideEffect函数

@Composable
fun WithOutSideEffectExample() {
    
    
    val count = remember {
    
     mutableStateOf(0) }
   
    Log.d("sideeffect", "Count is ${
      
      count.value}")
    
    Button(onClick = {
    
     count.value++ }) {
    
    
        Text(text = "Click here!")
    }
}

由上面的代码,您会注意到,当我们点击按钮时,count变量会增加,重新组合会发生,并且我们会看到一个logcat消息。这段代码工作得很好,但可能会对性能产生影响。

还记得吗?不要在 composable函数内运行任何non-composable代码,总是使用副作用进行处理。

使用SideEffect函数

@Composable
fun WithOutSideEffectExample() {
    
    
    val count = remember {
    
     mutableStateOf(0) }
  
    SideEffect{
    
    
           Log.d("sideeffect", "Count is ${
      
      count.value}")
    }
    
    Button(onClick = {
    
     count.value++ }) {
    
    
        Text(text = "Click here!")
    }
}

现在上面的代码看起来很好,因为我们将 logcat 代码移到了 SideEffect 块中。

derivedStateOf()

derivedStateOf 是 Jetpack Compose 中的一个函数,用于根据其他状态或派生状态的值计算值。

换句话说,derivedStateOf 是一个函数,允许您创建一个依赖于一个或多个其他状态的状态。

让我们看一个示例以更好地理解。

示例 1

fun DerivedStateExample() {
    
    

    var counter by remember {
    
     mutableStateOf(0) }
   
    val evenOdd by remember {
    
    
        derivedStateOf {
    
    
            if (counter % 2 == 0) "even"
            else "odd"
        }
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
    
    

        Text(text = "$counter", fontSize = 30.sp)
        SpacerHeight()
        Text(text = "count is $evenOdd", fontSize = 30.sp)
        SpacerHeight()
        Button(onClick = {
    
    
            counter++
        }) {
    
    
            Text(text = "Counter")
        }

    }

}

正如您在上述代码中所看到的,我们在每次点击按钮时都会增加 counter变量。在这里,counter是一个mutable state变量,并且基于这个counter变量,我们计算出奇数或偶数,并在Text中显示出来。

val oddEvent by remember {
    
    
    mutableStateOf(
        if (counter % 2 == 0)
            "even"
        else "odd"
    )
}

如果我们试图使用mutableStateOf来计算值,它永远不会起作用。

示例-2

@Composable
fun DerivedStateOfExample() {
    
    

    var numberOne by remember {
    
     mutableStateOf(0) }
    var numberTwo by remember {
    
     mutableStateOf(0) }
    val result by remember {
    
     derivedStateOf {
    
     numberOne + numberTwo } }

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
    
    

        TextField(value = "$numberOne", onValueChange = {
    
     numberOne = it.toIntOrNull() ?: 0 })
        SpacerHeight()
        TextField(value = "$numberTwo", onValueChange = {
    
     numberTwo = it.toIntOrNull() ?: 0 })
        SpacerHeight()
        Text(text = "Result is : $result", fontSize = 30.sp)
    }

}

如您在上例中所见,我们有两个TextField和它们的mutableState变量(numberOne,numberTwo)。现在借助于derivedStateOf,我们对这两个变量进行计算(相加),并在Text中显示结果。

猜你喜欢

转载自blog.csdn.net/u011897062/article/details/131281775
今日推荐