Android---Jetpack Compose学习007

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 需要能够合理地处理一些副作用。

\bullet 副作用的执行时机是明确的,例如在 Recomposition 时等。

\bullet 副作用的执行次数是可控的,不应该随着函数反复执行。

\bullet 副作用不会造成泄漏,例如对于注册要提供适当的时机取消注册。

组合函数的副作用

组合函数是主要是用来做 UI 声明的、描述的,只要你在可组合函数内做了与 UI 描述不相关的操作,这一类操作其实都属于副作用。

在 Compose 中可组合函数内部理应只做视图相关的事情,而不应该做函数返回之外的事情,如访问文件等,如果有,那这就叫做附带效应,以下操作全部都是危险的附带效应:

\bullet 写入共享对象的属性;

\bullet 更新 ViewModel 中的可观察项。

\bullet 更新共享偏好设置。

可组合函数应该是无副作用的,但是如果我们要在 Compose 里面使用可组合函数而且会产生附带效应,这时就需要使用 EffectAPI,以便以可预测的方式执行那些副作用。一个 effect,就是一个可组合函数,这个可组合函数不生成 UI,而是在组合完成时产生副作用。

组合函数的生命周期

这些 Effect API 是与我们组合函数的生命周期相关联的。可组合项的生命周期比 activity 和 fragment 的生命周期更简单,一般是进入组合、执行0次或者多次重组、退出组合。

\bullet Enter:挂载到树上,首次显示。

\bullet Composition:重组刷新 UI。

\bullet Leave:从树上移除,不再显示。

组合函数中没有自带的生命周期函数,想要监听其生命周期,需要使用 Effect(附带效应)API :

\bullet LaunchedEffect:第一次调用 Compose 函数的时候调用。

\bullet DisposableEffect:内部有一个 onDispose() 函数,当页面退出时调用。

\bullet SideEffect:compose 函数每次执行都会调用该方法。

LaunchedEffect

如果在可组合函数中进行耗时操作(副作用往往都是耗时操作,例如网络请求、I/O等),就需要将耗时操作放入协程中执行,而协程需要在协程作用域中创建,因此 Compose 提供了 LaunchedEffect 用于创建协程。

\bullet 当 LaunchedEffect 进入组件树时,会启动一个协程,并将 block 放入该协程中执行。

\bullet 当组合函数从视图树上 detach 时,协程还未被执行完毕,该协程也会被取消执行

\bullet 当 LaunchedEffect 在重组时其 key 不变,那 LaunchedEffect 不会被重新启动执行 block。

\bullet 当 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 类型变量并返回

\bullet produceState 创建了一个协程,但它也可用于观察非挂起的数据源

\bullet 当 produceState 进入 Composition 时,获取数据的任务被启动,当其离开 Composition 时,该任务被取消。

derivedStateOf

如果某个状态是从其它状态对象计算或派生得出的,请使用 derivedStateOf。作为条件的状态我们称为条件状态。当任意一个条件状态更新时,结果状态都会重新计算

snapshotFlow

使用 snapshotFlow 可以将 State 对象转换为 Flow。snapshotFlow 会运行传入的 block,并发出从块中读取的 State 对象的结果。当在 snapshotFlow 块中读取的 State 对象之一发生变化时,如果新值与之前发出的值不相等,Flow 会向其收集器发出新值。

  • 20
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

别偷我的猪_09

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值