Jetpack Compose 入门难点解疑_compose附带效应(3),2024年最新HarmonyOS鸿蒙组件化 面试

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新HarmonyOS鸿蒙全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img

img
img
htt

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注鸿蒙)
img

正文

组合中可组合项的生命周期。 进入组合,执行 0 次或多次重组,然后退出组合。

每一次composable(重组)就是调用一次可组合函数

四、remember与状态

由于 Compose 是声明式工具集,因此更新它的唯一方法是通过新参数调用同一可组合项。这些参数是界面状态的表现形式。每当状态更新时,都会发生重组。

1.remember

remember 会将对象存储在组合中,当调用 remember 的可组合项从组合中移除后,它会忘记该对象

为什么需要remember,是因为Compose使用了纯函数的形式表达UI(与flutter等框架使用对象不同),可组合函数本身可能会被多次调用,如果我们直接在方法体中声明属性,这个属性就会因为方法本身被多次调用从而丢失,因此我们需要一种让变量“持久化”的能力,remember就提供了这种能力,让某个变量从“Enter the Compotision”阶段一直保存到“leave the Composition”阶段。

被remember包裹住的变量,每一次组合的时候,取的都是同一个变量。

有时候,我们希望某个remember变量在恰当的时候发生变化,例如int类型的变量num变化的时候,自动生成对应的字符串,我们可以使用remember的key,当key发生变化的时候,remember的变量会重新生成。

var num by remember { mutableStateOf(0) }
val numString = remember(key1=num) {“我是数字$num”}

上述案例中,numString是受num影响的,如果num不变的情况下,numString取的值永远都是上一次生成的值,一旦num发生了变化,即remember中的key值变化,那么remember的lambda会重新执行来获取新值。

2.MutableState

mutableStateOf 会创建可观察的 MutableState,后者是与 Compose 运行时集成的可观察类型

interface MutableState : State {
override var value: T
}

如果 value 有任何变化,系统就会为用于读取 value 的所有可组合函数安排重组。

在可组合项中声明 MutableState 对象的方法有三种:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }(实际中最多使用)
  • val (value, setValue) = remember { mutableStateOf(default) }

这些声明是等效的,以语法糖的形式针对状态的不同用法提供。您选择的声明应该能够在您编写的可组合项中生成可读性最高的代码。

简单来说,MutableState对象的作用就是一种可以被Compose观察其变化的对象,当一个MutableState变化的时候,这个对象所在的所有重组作用域都会进入重组。

*关于重组作用域的概念此处不展开,你可以大致理解为MutableState所在的那个可组合函数

通常MutableState是和remember一起出现的,下面演示一个组件:

@Composable
fun MyButton(){

var num:Int by remember{ mutableStateOf(0) }

Column{
Button(onClick = { num++ }) {
Text(“点我加一”)
}
Text(“当前点击次数:$num”)
}

}

很容易看出来,这是一个竖向的布局,上面是一个按钮,点击之后,会让num变量+1,然后触发重组,导致其下面的Text的显示内容也+1。

可能很多初学者看到num的类型是Int很奇怪,会奇怪为什么Int的类型变化会导致重组,不是说只有MutableState变化才会触发重组吗,这是由于使用了by这个操作符对MutableState进行了委托,num的get和set方法本质上是修改了MutableState的内部的value

我们可以去除掉by操作符,代码会变成这样,本质是一样的:

@Composable
fun MyButton(){

val num: MutableState = remember{ mutableStateOf(0) }

Column{
Button(onClick = { num.value++ }) {
Text(“点我加一”)
}
Text(“当前点击次数:${num.value}”)
}

}

可以看出来,num的类型变成了MutableState,不能再对num修改,而是修改起内部的value,这样会导致Compose进行重组(也许你会好奇为什么会进行重组,这里大致的原理是每个重组作用域都会监听它内部所有的MutableState的value的变化,一旦他们发生了变化就会触发重组,是一个观察者模式的设计)。

实际开发中基本都是使用by的方式委托调用MutableState,因为不需要额外写.value。

五、解决附带效应

附带效应是指发生在可组合函数作用域之外的应用状态的变化,例如当进入页面的时候更新一下当前的位置,亦或者修改一下全局变量,如果直接在可组合函数里面引入这些附带效应的话,会让逻辑出现严重的问题,下面是两种常见的错误的附带效应的使用案例:

/**

  • 手机位置更新服务
    */
    object PhoneLocationUpdateService{
    //…
    fun updateMyLocation(){
    // TODO: 更新当前的位置
    }
    //…
    }

/**

  • 全局变量、可组合函数以外的变量
    */
    var globalParams:Int=0

@Composable
fun TestScreen(){

//❎的做法一
PhoneLocationUpdateService.updateMyLocation()
//❎错误的做法二
globalParams++

Column{
//…
}

}

为什么附带效应在Compose中存在问题?

这要结合Compose的生命周期来说,回归到生命周期的这张图中

如果我们希望可组合函数显示的时候,都让某个全局变量增加1的话,确实很容易直接在可组合函数的开头几句中直接让全局变量+1,但是需要重视的是,可组合函数会在生命周期期间多次重组的,也就是自身会被多次调用,这样就和我们需要的业务相违背了。

相似地,如果我们在AndroidView中的onLayout中插入某些访问全局变量的代码的话,可想而知会出现多大的问题(因为onLayout会频繁被调用而且次数未知)。

var globalParams:Int=0

class CrazyView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
//让人疯狂!!!
globalParams++
}

}

解决附带效应利器——SideEffect Api

如 Compose 编程思想 文档中所述,可组合项应该没有附带效应。如果您需要更改应用的状态,您应该使用 Effect API,以便以可预测的方式执行这些附带效应

换句话说,我们的可组合函数确实有些情况需要带一点附带效应,但是我们希望以"可预测的方式"的执行。

注:SideEffect较多,属于最难上手的部分,但是只要你搞懂了他们的使用场景和解决的问题,就可以大胆使用了。

1.LaunchedEffect:在某个可组合项的作用域内运行挂起函数

使用场景:希望在一个组合内进行异步操作

LaunchedEffect会提供一个协程作用域,这个作用域不会随着重组消失,它只会在该组合销毁的时候停止。

LaunchedEffect和remember一样,使用key作为是否重启的标志,当key发生变化的时候,会重新启动运行挂起函数

下图展示了一个组件显示3秒之后会弹出一个"我显示了"的文字的可组合函数

图中的key1传入了Unit,也就是LaunchedEffect不会重启,你可以通过改变key的方式让它重启,具体得看业务需要。

2.rememberCoroutineScope:获取组合感知作用域,以便在可组合项外启动协程

使用场景:希望在非重组作用域启动协程任务

Compose中并不都是重组作用域,有一些诸如点击回调的地方,我们也希望启动协程任务,这样LaunchedEffect就无法满足我们的需求了,因为LaunchedEffect是一个可组合函数,他无法在重组作用域以外的地方调用。

下面看看案例,我们在重组作用域使用rememberCoroutineScope()方法生成一个scope,这个scope的生命周期和组合的生命周期也是一致的,从组合出现到销毁,中间的重组并不会影响它,同时我们根据名字也可以知道,这个scope内部是被remember处理过,我们不用担心重组之后又生成一个Scope。

接着我们就可以在非重组作用域(图中是onClick回调)中使用协程来完成异步操作。

你看懂了吗,点击按钮的3秒后,Text就会显示一段文字。

3.rememberUpdatedState:在效应中引用某个值,该效应在值改变时不应重启

使用场景:LaunchedEffect中执行了一段异步操作之后,希望取到最新的方法参数的值

假设我们拥有这样一个可组合函数,他的逻辑希望是:3秒后显示传入的num。

实际上,当你在3秒内传入了不同的num,在3秒后显示的结果是第一次传入的num。

这是什么情况呢,还记得LaunchedEffect的设计吗,它的设计就是避免异步逻辑遭受重组的干扰,因此只有第一次传入的num会真正被LaunchedEffect的lambda拿走,其余的num都被LaunchedEffect自身的设计忽视了。

这个时候会有人想起,LaunchedEffect的key是可以让它重启的,于是会改造成这样:

每次num发生变化的时候,都重启LaunchedEffect,这样不就可以在3秒倒计时之后,取到的都是最新的num吗,最终结果来说这是没问题的,显示的也是最新的值,但是问题是:倒计时也重启了。

在这种场景下,就需要使用rememberUpdatedState()了,它本质上非常简单,让我们看看源码:

@Composable
fun rememberUpdatedState(newValue: T): State = remember {
mutableStateOf(newValue)
}.apply { value = newValue }

实际上就是把一个值缓存在一个MutableState里面而已,这样有什么用呢,我们看看改造后的代码:

我们继续看代码,使用rememberUpdatedState()num缓存在一个MutableState中,当LaunchedEffect内部的delay结束时,通过MutableState访问到了最新的值。

等等,为什么这个时候获取到的是最新的值呢,不是说LaunchedEffect不是不会受到重组影响吗,当然不会,还记得MutableStateby使用方式吗,我们访问rememberUpDatedNum实际上是访问了MutableState内部的value变量,MutableState自始至终都没发生过变化,而是它内部的value发生了变化,因此我们可以取到最新的值。

4.DisposableEffect:需要清理的效应

使用场景:当前可组合函数去订阅某些信息,而且可组合函数销毁的时候取消订阅

假设我们有一个这样的天气服务,可以通知所有的订阅者当前的天气。

interface WeatherListener{

fun onUpdate(weather:String)

}

object WeatherService{

private val observerList=mutableListOf()

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}”)

}

DisposableEffectLaunchedEffect很类似,都有key作为重启的标识,只是必须调用onDispose方法结尾,在onDispose中进行解绑操作。

5.derivedStateOf:将一个或多个状态对象转换为其他状态

使用场景:订阅可观察的列表变化、观察多个状态的变化等

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注鸿蒙)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

DisposableEffectLaunchedEffect很类似,都有key作为重启的标识,只是必须调用onDispose方法结尾,在onDispose中进行解绑操作。

5.derivedStateOf:将一个或多个状态对象转换为其他状态

使用场景:订阅可观察的列表变化、观察多个状态的变化等

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注鸿蒙)
[外链图片转存中…(img-DFSHbxUH-1713592113304)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值