Compose 附带效应
a. 纯函数
纯函数指的是函数与外界交换数据只能通过函数参数和函数返回值来进行,纯函数的运行不会对外界环境产生任何的影响。比如下面这个函数:
fun Add(a : Int, b : Int) : Int {
return a + b
}
“副作用”(side effect),指的是如果一个操作、函数或表达式在其内部与外界进行了互动,产生运算以外的其他结果,则该操作或表达式具有副作用。
最典型的情况,就是修改了外部环境的变量值。例如如下代码:Add() 函数执行它需要一个外部变量 a,先进行 ++ 操作,然 a + b 返回。只要这个函数一执行,外部变量 a 就会改变。而对于这个 a 所产生的改变,这个就叫做副作用。
var a
fun Add(b : Int) : Unit{
a++
return a + b
}
因此,组合函数也是一个函数,那么它也分为有副作用的和没副作用的。而组合函数的副作用和其它函数还有一些差异。
组合函数的特点
a. 执行顺序不定;b. 可以并行运行;c. 可能会非常频繁地运行
处理副作用
虽然我们不希望函数执行中出现副作用,但现实情况是有一些逻辑只能作为副作用来处理。例如一些 IO 操作、计时、日志埋点等,这些都是会对外界或收到外界影响的逻辑,不能无限制的反复执行。所以 Compose 需要能够合理地处理一些副作用。
副作用的执行时机是明确的,例如在 Recomposition 时等。
副作用的执行次数是可控的,不应该随着函数反复执行。
副作用不会造成泄漏,例如对于注册要提供适当的时机取消注册。
组合函数的副作用
组合函数是主要是用来做 UI 声明的、描述的,只要你在可组合函数内做了与 UI 描述不相关的操作,这一类操作其实都属于副作用。
在 Compose 中可组合函数内部理应只做视图相关的事情,而不应该做函数返回之外的事情,如访问文件等,如果有,那这就叫做附带效应,以下操作全部都是危险的附带效应:
写入共享对象的属性;
更新 ViewModel 中的可观察项。
更新共享偏好设置。
可组合函数应该是无副作用的,但是如果我们要在 Compose 里面使用可组合函数而且会产生附带效应,这时就需要使用 EffectAPI,以便以可预测的方式执行那些副作用。一个 effect,就是一个可组合函数,这个可组合函数不生成 UI,而是在组合完成时产生副作用。
组合函数的生命周期
这些 Effect API 是与我们组合函数的生命周期相关联的。可组合项的生命周期比 activity 和 fragment 的生命周期更简单,一般是进入组合、执行0次或者多次重组、退出组合。
Enter:挂载到树上,首次显示。
Composition:重组刷新 UI。
Leave:从树上移除,不再显示。
组合函数中没有自带的生命周期函数,想要监听其生命周期,需要使用 Effect(附带效应)API :
LaunchedEffect:第一次调用 Compose 函数的时候调用。
DisposableEffect:内部有一个 onDispose() 函数,当页面退出时调用。
SideEffect:compose 函数每次执行都会调用该方法。
LaunchedEffect
如果在可组合函数中进行耗时操作(副作用往往都是耗时操作,例如网络请求、I/O等),就需要将耗时操作放入协程中执行,而协程需要在协程作用域中创建,因此 Compose 提供了 LaunchedEffect 用于创建协程。
当 LaunchedEffect 进入组件树时,会启动一个协程,并将 block 放入该协程中执行。
当组合函数从视图树上 detach 时,协程还未被执行完毕,该协程也会被取消执行。
当 LaunchedEffect 在重组时其 key 不变,那 LaunchedEffect 不会被重新启动执行 block。
当 LaunchedEffect 在重组时其 key 发生了变化,则 LaunchedEffect 会执行 cancel 后,再重新启动一个新协程执行 block。
示例:LaunchedEffect 在初次进入组件树时,就会启动一个协程,调用 block 块执行
1. LaunchedEffectSample.kt
@Composable
fun ScaffoldSample(
state : MutableState<Boolean>,
scaffoldState : ScaffoldState = rememberScaffoldState()
){
// TODO 当我启动这个应用时,组件一开始加载进来,LaunchedEffect() 就会启动一个协程,执行它的 block 块
//TODO 当 key = state.value 发生改变时(点击按钮时改变),就会启动协程
LaunchedEffect(state.value){
// 开启一个弹窗,TODO 是一个 block 块
scaffoldState.snackbarHostState.showSnackbar(
// 弹窗内容
message = "Error message",
actionLabel = "Retry message"
)
}
// TODO 脚手架
Scaffold(
scaffoldState = scaffoldState,
// 顶部标题栏区域
topBar = {
TopAppBar(
title = { Text(text = "脚手架示例!")}
)
},
// 屏幕内容区域
content = {
Box(
modifier = Modifier.fillMaxSize(), // 填充父容器
contentAlignment = Alignment.Center // 居中
){
Button(
onClick = {
//TODO 点击按钮时,弹窗,改变 state 的值。一个动画效果,为耗时操作,即附带效应
state.value = !state.value
}
) {
Text(text = "Click it!")
}
}
}
)
}
@Composable
fun LaunchedEffectSample(){
val state = remember { mutableStateOf(false) }
ScaffoldSample(state)
}
2. MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeSideEffectsTheme {
LaunchedEffectSample()
}
}
}
}
上面的示例中,当我们启动 App 时就会让 LaunchedEffect 进入组件树时,启动一个协程,并将 block 放入该协程中执行。可以做如下改变,让进入 App 时不执行 block 块。修改 LaunchedEffect 代码如下:
if(state.value){
LaunchedEffect(scaffoldState.snackbarHostState){
// 开启一个弹窗,TODO 是一个 block 块
scaffoldState.snackbarHostState.showSnackbar(
// 弹窗内容
message = "Error message",
actionLabel = "Retry message"
)
}
}
rememberCoroutineScope
由于 LauncedEffect 本身就是个可组合函数,因此只能在其他可组合函数中使用。想要在可组合项外启动协程,且需要对这个协程存在作用域限制,以便协程在退出组合后自动取消,可以使用 rememberCoroutineScope。
此外,如果需要手动控制一个或多个协程的生命周期,请使用 rememberCoroutineScope。拿到协程的作用域。
示例:
1. RememberCoroutineScopeSample.kt
@Composable
fun ScaffoldSample(){
val scaffoldState = rememberScaffoldState()
// TODO 拿到协程作用域,启动多个协程
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
//TODO 左侧抽屉栏,点击了菜单按钮时,弹出
drawerContent = {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "抽屉组件中的内容")
}
},
// 顶部标题栏区域
topBar = {
TopAppBar(
// 左上角的菜单栏按钮,点击后左侧弹窗
navigationIcon = {
IconButton(
onClick = {
// TODO 点击菜单按钮时,弹出左侧抽屉栏
// TODO 1 启动一个协程
scope.launch {
// 以动画的形式打开这个抽屉
scaffoldState.drawerState.open()
}
}
) {
// 菜单按钮
Icon(imageVector = Icons.Filled.Menu, contentDescription = null)
}
},
title = { Text(text = "脚手架示例!")}
)
},
// 屏幕内容区域
content = {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "屏幕内容区域")
}
},
// TODO 右下角的悬浮按钮
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text(text = "悬浮按钮") },
onClick = {
// TODO 2 启动一个协程
scope.launch {
// 弹窗
scaffoldState.snackbarHostState.showSnackbar("点击了悬浮按钮")
}
}
)
}
)
}
2. MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeSideEffectsTheme {
//LaunchedEffectSample()
ScaffoldSample()
}
}
}
}
在上面代码中,我们通过 val scope = rememberCoroutineScope() 拿到协程作用域,以此来控制多个协程生命周期
rememberUpdatedState
如果 key 值有更新,那么 LaunchedEffect 在重组时就会被重新启动。但是有时候需要在 LaunchedEffect 中使用最新的参数值,但是又不想重新启动 LaunchedEffect,此时就需要用到 rememberUpdatedState。
rememberUpdatedState 的作用是给某个参数创建一个引用,来跟踪这些参数,并保证其值被使用时是最新值,参数被改变时不重启 effect。
示例:RememberUpdatedStateSample.kt
@Composable
fun LandingScreen(onTimeOut : () -> Unit){
// TODO onTimeOut() 转换成一个状态了
val currentOnTimeout by rememberUpdatedState(newValue = onTimeOut)
//TODO key1 = Unit 表示这个 key 值不会变
LaunchedEffect(key1 = Unit){
Log.d("HL", "LaunchedEffect")
repeat(10){
delay(1000)
Log.d("HL", "delay ${it + 1}s")
}
//
//onTimeOut()
currentOnTimeout()
}
}
@Composable
fun RememberUpdatedStateSample(){
val onTimeOut1 : () -> Unit = { Log.d("HL", "landing timeout 1") }
val onTImeOut2 : () -> Unit = { Log.d("HL", "landing timeout 2") }
// 创建一个 state, 默认值为 onTimeOut1
val changeOnTimeOutState = remember { mutableStateOf(onTimeOut1) }
Column {
Button(
onClick = {
// TODO 点击按钮时,改变 changeOnTimeOutState 的值
if(changeOnTimeOutState.value == onTimeOut1){
changeOnTimeOutState.value = onTImeOut2
}else{
changeOnTimeOutState.value = onTimeOut1
}
}
) {
Text(text = "choose onTimeOut ${if(changeOnTimeOutState.value == onTimeOut1) 1 else 2}")
}
//TODO changeOnTimeOutState.value == OnTimeOut1 / OnTimeOut2
LandingScreen(changeOnTimeOutState.value)
}
}
DisposableEffect
DisposableEffect 也是一个可组合函数,当 DisposableEffect 在其 key 值变化或者组合函数离开组件树时,会取消之前启动的协程,并会在取消协程前调用其回收方法进行资源回收相关的操作,可以对一些资源等进行清理。
示例:当开关按钮打开时,拦截返回按钮。
DisposableEffectSample.kt
// 对返回进行一个拦截
@Composable
fun BackHandler(
backDispatcher : OnBackPressedDispatcher,
onBack : () -> Unit
){
// onBack 包装成一个状态, TODO 以便可以随时替换为其它的函数
val currentOnBack by rememberUpdatedState(newValue = onBack)
val backCallback = remember {
object : OnBackPressedCallback(true){
override fun handleOnBackPressed() {
//onBack()
currentOnBack()
}
}
}
DisposableEffect(key1 = backDispatcher){
// 开关打开,添加拦截 backCallback
backDispatcher.addCallback(backCallback)
// 执行时机为:BackHandler 从组件树中移除,也就是 switch 开关关掉的时候
onDispose {
Log.d("HL", "onDispose")
// 开关一关,从组件树中移除
backCallback.remove()
}
}
}
@Composable
fun DisposableEffectSample(backDispatcher : OnBackPressedDispatcher){
// TODO 设置一个状态
var addBackCallback by remember { mutableStateOf(false) }
Row {
// 开关按钮
Switch(
checked = addBackCallback, // 默认选中或不选中
onCheckedChange = {
// 当点击开关进行切换的时候,调用这里的代码
addBackCallback = !addBackCallback
}
)
Text(text = if (addBackCallback) "Add back callback" else "Not add back callback")
}
if(addBackCallback){ // TODO 打开开关,BackHandler() 执行
BackHandler(backDispatcher){
Log.d("HL", "onBack")
}
}
}
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeSideEffectsTheme {
//LaunchedEffectSample()
//ScaffoldSample()
//RememberUpdatedStateSample()
DisposableEffectSample(onBackPressedDispatcher)
}
}
}
}
SideEffect
SideEffect 是简化版的 DisposableEffect,SideEffect 并未接收任何 key 值,所以,每次重组,就会执行其 block。当不需要 onDispose、不需要参数控制时使用 SideEffect。SideEffect 主要用来与非 Compose 管理的对象共享 Compose 状态。
SideEffect 在组合函数被创建并载入视图树后才会被调用。
例如,我们的分析库可能允许通过将自定义元数据(在此示例中为“用户属性”)附加到所有后续分析事件,来细分用户群体。如需将当前用户的用户类型传递给你的分析库,请使用 SideEffect 更新其值。
prodeceState
produceState 可以将非 Compose(如 Flow、LiveData 或 RxJava)状态转换为 Compose 状态。它接收一个 lambda 表达式作为函数体,能将这些入参经过一些操作后生成一个 State 类型变量并返回。
produceState 创建了一个协程,但它也可用于观察非挂起的数据源。
当 produceState 进入 Composition 时,获取数据的任务被启动,当其离开 Composition 时,该任务被取消。
derivedStateOf
如果某个状态是从其它状态对象计算或派生得出的,请使用 derivedStateOf。作为条件的状态我们称为条件状态。当任意一个条件状态更新时,结果状态都会重新计算。
snapshotFlow
使用 snapshotFlow 可以将 State 对象转换为 Flow。snapshotFlow 会运行传入的 block,并发出从块中读取的 State 对象的结果。当在 snapshotFlow 块中读取的 State 对象之一发生变化时,如果新值与之前发出的值不相等,Flow 会向其收集器发出新值。