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-handlers
和side-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中显示结果。