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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念

Jetpack Compose 入门难点解疑


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

谷歌和苹果分别维护着两个地球上最大的手机操作系统:Android和IOS。长期以来,由于编程语言的特性,手机端一直都是使用命令式布局(偏向面向对象的开发方式)来开发UI,但是随着手机UI逐渐复杂和动态化,原有的方式暴露了其瓶颈,越来越多的手机端工程师抱怨,实现产品要求的效果越来越难了。于是为了适应市场的要求,谷歌和苹果分别推出了原生的声明式布局开发框架,苹果端是SwiftUi,安卓端就是本期的主角-Jetpack Compose

如果你从来没听说过Jetpack Compose,笔者建议你先阅读一下官方的简介和开发文档,因为本期节目并不会从0开始介绍这个框架,而是带领新人攻克最难的几个入门门槛。

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

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


一、可组合函数与附带效应


image.png

可组合函数是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的生命周期


image.png

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

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


四、remember与状态


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


1.remember


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

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

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

image.png

image.png

有时候,我们希望某个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。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
|
存储 缓存 Android开发
安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【远程url】音频,搭配Okhttp库进行下载和缓存,播放完随机播放下一首
这是一个Kotlin项目,使用Jetpack Compose和ExoPlayer框架开发Android应用,功能是播放远程URL音频列表。应用会检查本地缓存,如果文件存在且大小与远程文件一致则使用缓存,否则下载文件并播放。播放完成后或遇到异常,会随机播放下一首音频,并在播放前随机设置播放速度(0.9到1.2倍速)。代码包括ViewModel,负责音频管理和播放逻辑,以及UI层,包含播放和停止按钮。
180 0
|
2月前
|
存储 数据库 Android开发
安卓Jetpack Compose+Kotlin,支持从本地添加音频文件到播放列表,支持删除,使用ExoPlayer播放音乐
为了在UI界面添加用于添加和删除本地音乐文件的按钮,以及相关的播放功能,你需要实现以下几个步骤: 1. **集成用户选择本地音乐**:允许用户从设备中选择音乐文件。 2. **创建UI按钮**:在界面中创建添加和删除按钮。 3. **数据库功能**:使用Room数据库来存储音频文件信息。 4. **更新ViewModel**:处理添加、删除和播放音频文件的逻辑。 5. **UI实现**:在UI层支持添加、删除音乐以及播放功能。
133 1
|
2月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
2月前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
173 2
|
2月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
101 2
|
2月前
|
监控 Android开发 数据安全/隐私保护
安卓kotlin JetPack Compose 实现摄像头监控画面变化并录制视频
在这个示例中,开发者正在使用Kotlin和Jetpack Compose构建一个Android应用程序,该程序 能够通过手机后置主摄像头录制视频、检测画面差异、实时预览并将视频上传至FTP服务器的Android应用
141 1
|
2月前
深入了解 Jetpack Compose 中的 Modifier
深入了解 Jetpack Compose 中的 Modifier
26 0
|
2月前
|
Android开发
Jetpack Compose: Hello Android
Jetpack Compose: Hello Android
20 0
|
2月前
|
安全 网络安全 API
kotlin安卓开发JetPack Compose 如何使用webview 打开网页时给webview注入cookie
在Jetpack Compose中使用WebView需借助AndroidView。要注入Cookie,首先在`build.gradle`添加WebView依赖,如`androidx.webkit:webkit:1.4.0`。接着创建自定义`ComposableWebView`,通过`CookieManager`设置接受第三方Cookie并注入Cookie字符串。最后在Compose界面使用这个自定义组件加载URL。注意Android 9及以上版本可能需要在网络安全配置中允许第三方Cookie。
279 0
|
2月前
|
Android开发 Kotlin
kotlin安卓开发【Jetpack Compose】:封装SnackBarUtil工具类方便使用
GPT-4o 是一个非常智能的模型,比当前的通义千问最新版本在能力上有显著提升。作者让GPT开发一段代码,功能为在 Kotlin 中使用 Jetpack Compose 框架封装一个 Snackbar 工具类,方便调用
108 0

相关实验场景

更多