我正在参与「启航计划」

Jetpack Compose 入门难点解疑

近些年声明式布局开发办法逐渐从网页端延展到了手机端,说到底仍是声明式太香了,其代码更加明晰、简练,并且更接近于自然言语的表达办法。这使得代码易于了解和保护,降低了开发人员的心智负担。

谷歌和苹果别离保护着两个地球上最大的手机操作系统:Android和IOS。长期以来,由于编程言语的特性,手机端一直都是运用指令式布局(倾向面向目标的开发办法)来开发UI,可是跟着手机UI逐渐杂乱和动态化,原有的办法暴露了其瓶颈,越来越多的手机端工程师诉苦,实现产品要求的效果越来越难了。所以为了习惯市场的要求,谷歌和苹果别离推出了原生的声明式布局开发结构,苹果端是SwiftUi,安卓端便是本期的主角-Jetpack Compose

假如你从来没听说过Jetpack Compose,笔者主张你先阅览一下官方的简介和开发文档,由于本期节目并不会从0开始介绍这个结构,而是带领新人攻克最难的几个入门门槛。

  • Jetpack Compose 简介 | Android Developers (google.cn)
  • Jetpack Compose 开发文档 | Android Developers (google.cn)

假如你已经大致读完了上面两个文档,你仍然回到这篇文章,那阐明你遇到了笔者一开始遇到的问题:Jetpack Compose的文档实在写的太烂了,充满了机翻,并且比较难的概念仅仅一笔带过。

本章会手把手带你了解Jetpack Compose初学最难了解的几个难点。

一、可组合函数与顺便效应

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

可组合函数是Compose中描绘UI的函数,你能够把它类比成HTML,可组合函数运用kotlin作为开发言语。

关于可组合函数,必须有以下特色:

  • 此函数带有 @Composable 注释。一切可组合函数都必须带有此注释;此注释可奉告 Compose 编译器:此函数旨在将数据转换为界面。

  • 此函数承受数据。可组合函数能够承受一些参数,这些参数可让运用逻辑描绘界面。在本例中,咱们的 widget 承受一个 String,因而它能够按称号问候用户。

  • 此函数能够在界面中显现文本。为此,它会调用 Text() 可组合函数,该函数实际上会创建文本界面元素。可组合函数经过调用其他可组合函数来宣布界面层次结构。

  • 此函数不会回来任何内容。宣布界面的 Compose 函数不需求回来任何内容,由于它们描绘所需的屏幕状况,而不是结构界面 widget。

  • 可组合函数快速、幂等且没有顺便效应

    • 运用同一参数屡次调用此函数时,它的行为办法相同,并且它不运用其他值,如全局变量或对 random() 的调用。
    • 此函数描绘界面而没有任何副效果,如修正特点或全局变量。

这儿呈现了一个概念:幂等,或许有人第一次接触到这个词,我供给一下关于这个词的解释:

在编程中,”幂等”是指一个操作或函数,无论履行多少次,成果都是相同的。换句话说,关于给定的输入,屡次履行相同的操作或函数不会产生额定的影响或副效果。

假定你的函数改动了外部的变量或许拜访了外部的变量,那它就不是幂等的,例如下面的函数:

var a:Int=1
fun nonIdempotent(){
    a++
}

很显然,每次调用nonIdempotent()办法的成果都是不相同的,这种便是”不幂等“的函数。

相同的,咱们再来看看幂等的函数:

fun idempotent(a:Int):Int{
    return a+1
}

假如我传入的a不相同,每次回来的成果都不相同,还算幂等吗?当然算,咱们说幂等的前提是坚持参数共同,假如参数共同的状况下,成果永远都是a+1,因而办法是幂等的。

下面再看一个函数,请问这是幂等的吗:

fun idempotentOrNonIdempotent(a:Int):Int{
    print("${a+1}")
    return a+1
}

或许你会觉得这是幂等的,由于成果是共同的,可是这个函数却是不幂等的,由于print会对控制台输出日志,这属于对函数外部产生了影响,而对外部产生影响属于顺便效应,因而也是不幂等的。

顺便效应是指发生在可组合函数效果域之外的运用状况的改动。由于可组合项的生命周期和特点(例如不行预测的重组、以不同顺序履行可组合项的重组或能够放弃的重组),可组合项在抱负状况下应该是无顺便效应的。

回到Compose中来,为什么Compose的可组合函数要强调幂等且没有顺便效应呢?由于Compose是没有目标这一个概念的,它是用纯粹的函数来表达UI,因而UI的改写便是从头调用一次可组合函数,改写进程由Compose的智能重组机制主动完结,关于这个机制咱们接下来才会提到,你只需求了解一个概念:即Compose的UI改写便是从头调用一次可组合函数,并且调用的次数和机遇是不确定的即可。

基于这个因素,假如咱们的可组合函数里边呈现了顺便效应的状况,就会导致顺便效应在不恰当的机遇呈现,例如下面的代码:

@Composable
fun MyScreen(
    title:String
){
    Log.d("UI日志","MyScreen")
    Column{
        Text(title)
    }
}

或许你的原意仅仅想输出一个日志,检查MyScreen的呈现机遇,可是这样在Compose中属于经典的过错。这样写的成果是每逢MyScreen改写的时分,都会输出一遍日志,如何处理顺便效应的问题咱们接下来再讲,现在只需求读者留一个概念:千万要注意顺便效应

二、重组与智能重组

在指令式界面模型中,如需更改某个 widget,您能够在该 widget 上调用 setter 以更改其内部状况。在 Compose 中,您能够运用新数据再次调用可组合函数。

这样做会导致函数进行重组,系统会依据需求运用新数据从头制作函数宣布的 widget。Compose 结构能够智能地仅重组已更改的组件。

例如,假定有以下可组合函数,它用于显现一个按钮:

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
  Button(onClick = onClick) {
    Text("I've been clicked $clicks times")
   }
}

每次点击该按钮时,调用方都会更新 clicks 的值。Compose 会再次调用 lambda 与 Text 函数以显现新值;此进程称为“重组”。不依赖于该值的其他函数不会进行重组。

如前文所述,重组整个界面树在核算上成本昂扬,由于会耗费核算才能并缩短电池续航时刻。Compose 运用智能重组来处理此问题。

所谓的智能重组便是:Compose依据可组合函数的参数来决定是否进行重组。

也便是说,每一次可组合函数被调用的时分,他会检查一切传入的参数,假如本次传入的参数和上一次传入的参数都是相同的话(这儿指的相同是指结构性相等,在kotlin中指的是==,在java中指的是调用目标的equals()办法) ,那么Compose就会略过调用这个可组合函数,以达到最快的重组功率。

让咱们回到这个可组合函数,假如他的父级可组合函数由于某种原因触发了重组,那么Compose就会测验调用MyScreen()来完结改写,假如title参数没有发生改动的话,Compose实际上就会略过MyScreen的改写。

@Composable
fun MyScreen(
    title:String
){
    Column{
        Text(title)
    }
}

三、Compose的生命周期

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

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

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

四、remember与状况

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

1.remember

remember 会将目标存储在组合中,当调用 remember 的可组合项从组合中移除后,它会忘掉该目标

为什么需求remember,是由于Compose运用了纯函数的办法表达UI(与flutter等结构运用目标不同),可组合函数本身或许会被屡次调用,假如咱们直接在办法体中声明特点,这个特点就会由于办法本身被屡次调用从而丢失,因而咱们需求一种让变量“耐久化”的才能,remember就供给了这种才能,让某个变量从“Enter the Compotision”阶段一直保存到“leave the Composition”阶段。

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

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

有时分,咱们期望某个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<T> : State<T> {
  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<Int> = 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的生命周期来说,回归到生命周期的这张图中

妈!Jetpack 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秒之后会弹出一个”我显现了”的文字的可组合函数

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

图中的key1传入了Unit,也便是LaunchedEffect不会重启,你能够经过改动key的办法让它重启,具体得看业务需求。

2.rememberCoroutineScope:获取组合感知效果域,以便在可组合项外发动协程

运用场景:期望在非重组效果域发动协程任务

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

下面看看事例,咱们在重组效果域运用rememberCoroutineScope()办法生成一个scope,这个scope的生命周期和组合的生命周期也是共同的,从组合呈现到毁掉,中间的重组并不会影响它,一起咱们依据名字也能够知道,这个scope内部是被remember处理过,咱们不用忧虑重组之后又生成一个Scope。

接着咱们就能够在非重组效果域(图中是onClick回调)中运用协程来完结异步操作。

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

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

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

运用场景:LaunchedEffect中履行了一段异步操作之后,期望取到最新的办法参数的值

假定咱们拥有这样一个可组合函数,他的逻辑期望是:3秒后显现传入的num。

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

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

这是什么状况呢,还记得LaunchedEffect的规划吗,它的规划便是防止异步逻辑遭受重组的干扰,因而只有第一次传入的num会真正被LaunchedEffect的lambda拿走,其他的num都被LaunchedEffect本身的规划忽视了。

这个时分会有人想起,LaunchedEffect的key是能够让它重启的,所以会改造成这样:

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

每次num发生改动的时分,都重启LaunchedEffect,这样不就能够在3秒倒计时之后,取到的都是最新的num吗,终究成果来说这是没问题的,显现的也是最新的值,可是问题是:倒计时也重启了。

在这种场景下,就需求运用rememberUpdatedState()了,它实质上非常简略,让咱们看看源码:

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

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

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

咱们持续看代码,运用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<WeatherListener>()
    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:##### 将一个或多个状况目标转换为其他状况

运用场景:订阅可调查的列表改动、调查多个状况的改动等

有时分咱们期望某个状况发生改动的时分,会改动别的一个状况的值,一般能够运用rememberkey来完结这个业务,例如下图,showText的值会在num改动的时分从头生成。

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

可是有些可调查的状况咱们是无法运用为rememberkey的,由于改动并不是发生在它本身的值的改动,而是其内部的值发生了改动,例如常见的mutableListOf()生成的列表。

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

为什么会没用呢,由于remember比较的额是目标本身,而不是目标内部的内容,关于list来说,它从来没有改动为其他引证,咱们只改动它内部的元素,因而remember是无法感知到list的改动的,这时分咱们就需求运用derivedStateOf来感知。

妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

derivedStateOf传入的lambda里边的任意一个MutableState发生改动的时分,就会从头生成一个新值。

其他类内部包含了其他State的也能够经过这种办法来调查其改动。

总结

笔者大致讲了一下Compose的根底概念和入门难点,期望大家在入门Compose的进程中少走弯路,多了解不同api在适宜的场景下的效果,后续会出更多Compose和其他安卓开发的文章,请重视订阅点赞。