欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > Compose 实践与探索一 —— 关键知识与概念详解

Compose 实践与探索一 —— 关键知识与概念详解

2025/3/16 2:21:35 来源:https://blog.csdn.net/tmacfrank/article/details/146165769  浏览:    关键词:Compose 实践与探索一 —— 关键知识与概念详解

本篇作为 Compose 探索与实践系列的第一篇文章,会从宏观上介绍 Compose 的常用概念、层级结构,并对一些关键知识做一些铺垫。闲言少叙,现在开始!

1、Jetpack Compose 简介

1.1 为何推出 Compose

传统 Android UI 开发有三大痛点:

  1. View.java 具有沉重的历史包袱:经过十余年的开发迭代,View.java 文件内的代码已经超过 3 万行,非常臃肿,造成子类视图功能不合理(比如 Button 继承自 TextView,但是 Button 不需要的粘贴版功能也被继承了),同时难以修改已经发布的 API 接口
  2. XML 布局具有局限性:首先,XML 类型不安全,运行时才能发现布局错误;其次,XML 动态布局困难,需要获取视图对象后以命令式代码调用对象方法驱动 UI 变更
  3. 状态管理困难:需要手动维护视图状态与数据状态,并且在异步回调中容易发生内存泄漏,交互处理也复杂,需要进行多视图间的状态同步

Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发。

Compose 的优势:

  • 更少的代码:使用更少的代码实现更多的功能,并且可以避免各种 bug,从而使代码简洁且易于维护
  • 直观:只需描述界面,Compose 会负责处理剩余的工作。应用状态变化时,界面会自动更新
  • 加快应用开发:兼容性现有的所有代码,方便随时采用。借助实时预览和全面的 Android Studio 支持,实现快速迭代
  • 功能强大:凭借对 Android 平台 API 的直接访问和对于 Material Design、深色主题、动画等的内置支持,创建精美的应用

Compose 使用声明式 UI,描述 UI 应该是什么样子,而不是如何构建(如何转换到对应的状态),这样可以做到只进行一次声明,而无需手动更新 UI。而传统的命令式 UI 需要在获取到最新数据后手动将数据设置给 UI,比如 textView.setText("xxx")

以下是传统 View 系统与 Jetpack Compose 的对比:

特性传统 View 系统Jetpack Compose
开发模式命令式,开发效率较低声明式,DSL 开发效率较高
代码组织XML+Java/Kotlin纯 Kotlin
状态管理手动同步自动响应
预览支持有限预览实时交互预览
体系结构类职责不单一,继承关系不合理函数式编程思想,通过组合规避了面向对象的弊端
版本管理依赖系统版本,问题修复不及时独立迭代,具有良好的系统兼容性

Data Binding 作为 Compose 诞生之前的一种声明式 UI 方案可以让 XML 自己“动”起来,Google 官方将其视为一种声明式框架,说明声明式 UI 的优势早已被官方认可,推出 Compose 也是为了彻底解决传统 View 体系的弊端。

1.2 独立于平台

Google 近些年一直在推独立于系统,可以单独发布的组件。Jetpack 库就是可以独立更新的库,像我们熟知的 AppCompat、ConstraintLayout、RecyclerView 等等组件都是 Jetpack 库的内容。它们都不依赖于 Android 系统版本,可以独立发布。相比于 TextView、ImageView 这种写在 Android 系统内的组件代码只有等系统更新时才可以更新,一旦有 Bug 需要修改或者新 Feature 需要添加就必须等整个大系统更新才行,像 Jetpack 这种独立于系统发布的组件可以做到及时更新,无需依赖系统版本。

Compose 作为 Jetpack 中较新的成员,它不仅像其他成员那样独立于 Android 的系统版本,还独立于 Android 平台。独立于平台的目的是为了实现多平台。Compose 的初衷并不是像 Flutter 那样为了让 Android、iOS 都使用 Flutter 进行开发,而是为了像在 Windows、Mac 上可以让 Android Studio 的模拟器呈现更好的预览效果。

Flutter 的目的是跨平台(Cross-Platform),而 Kotlin 的目的是多平台(Multi-Platform)。对于 JetBrains 而言,将 Kotlin 跨到 iOS 是优先级很低的工作,它主要是想实现桌面版(Mac、Windows、Linux)、Web 的多平台。

Compose 多平台在底层还是要用到原生 API 的,就像 JVM 支持跨平台有 Linux、Windows、Mac 等不同的版本。在 Android 平台进行绘制时,底层还是会用到 drawText()、drawTextRun() 等 Android 原生 API。

Compose 不是魔法,在底层是绕不开原生的,毕竟是要做 UI 交互的。

1.3 由传统开发转向 Compose

用了十几年的 View 系统,突然转向 Compose 是会不太习惯的。比如说,传统的 FrameLayout、LinearLayout 都没了,RecyclerView 与 ViewPager 也不见了踪影,甚至连常用的第三方库在 Compose 中也要换成新的库。这里我们先简单聊聊,Compose 中那些 View 系统的“平替”,算是对 Compose 的组件先有个印象。

原生开发与 Jetpack 中常用的组件在 Compose 中的平替:

  • FrameLayout - Box
  • LinearLayout - Row & Column
  • RelativeLayout - Box(通过 Modifier 控制位置关系)
  • ConstraintLayout - Compose 与 ConstraintLayout 团队配合将 ConstraintLayout 与 MotionLayout 的逻辑移植到 Compose 中
  • RecyclerView - LazyColumn & LazyRow
  • ScrollView - Modifier.horizontalScroll() & Modifier.verticalScroll()
  • ViewPager - Pager

原生开发中,图片处理是一个非常常见的话题,那么在 Compose 中它变成什么样了呢?下面我们再简单聊聊这个话题。

位图在 Android 原生会使用 Bitmap 表示,而在 Compose 中,则使用 ImageBitmap 表示。此外还有表示矢量图的 ImageVector。

Compose 的图片库不再由 Glide、Picasso、Fresco 统治,而是 Google 官方推荐的 Coil(作者解释:Coroutine Image Loader)。它由 Kotlin 实现,面向协程,且不面向 View 系统。

此外,Google 官方还有一个专门为 Compose 进行功能扩展的库 —— accompanist,它最初由 Chris Banes 创建,主要是对图片加载进行支持,对 Picasso、Glide 和 Coil 进行功能扩展以支持 Compose 的图片加载。此外,它的 Pager 可以替代 Jetpack 的 ViewPager。这个库的作者也解释过库名的由来,大意是 compose 有作曲的意思,作为一个为 Compose 提供辅助功能的库,使用伴奏者 accompanist 正有辅助作曲之意。

accompanist 作为 Compose 库的一个未定功能孵化库,它的功能没 Compose 那么稳定。有可能一些好的、稳定的功能最后会合到 Compose 中,当然也有一些功能最终会被丢弃。比如 Picasso 最早就被移除了,因为使用它加载图片的人太少了;Glide 也有类似的原因也被移除了;Coil 库后续提供了对 Compose 的支持,因此也被移除。

2、初识 Modifier

2.1 Modifier 的特点

Modifier 在整个 Compose 体系中占有非常重要的位置,对组件属性的设置、组件的测量、布局以及绘制、组件的事件处理与手势识别等等,都由 Modifier 来控制,后续我们会用几篇文章的篇幅来介绍各种 Modifier,现在我们只通过几个简单的示例,来对 Modifier 有一个初步的认识。

比如,以前我们使用 XML 进行原生开发时,常常会为组件设置内边距 padding 和外边距 margin。到了 Compose 中,不再有 margin 作为外边距这个概念了,layout_padding 属性也变成了 Modifier 的 padding 函数,使用 Modifier.padding() 就可以同时实现内边距和外边距的效果了,我们一步一步来看具体如何实现。

首先,我们在 Column 中放一个 Text:

@Composable
fun PaddingPreview() {Column(Modifier.background(Color.Red)) {Text("Jetpack Compose", Modifier.background(Color.Blue))}
}

效果是显示一个蓝色背景的 Text 组件:

在这里插入图片描述

然后我们为 Text 的 Modifier 添加 padding() 指定一个 10dp 的内边距:

@Composable
fun PaddingPreview() {Column(Modifier.background(Color.Red)) {Text("Jetpack Compose", Modifier.background(Color.Blue).padding(10.dp))}
}

可以看到 Text 的内边距确实增加了:

在这里插入图片描述

然后我们修改 padding() 的调用位置,把它放到 background() 之前,为 Text 设置 10dp 的外边距:

@Composable
fun PaddingPreview() {Column(Modifier.background(Color.Red)) {Text("Jetpack Compose", Modifier.padding(10.dp).background(Color.Blue))}
}

可以看到,作为底部背景的 Column 的红色显示出来了,说明这样确实为 Text 增加了外边距,而且上一个版本为 Text 设置的内边距没有了:

在这里插入图片描述

假如想同时为 Text 设置内外边距,那么就在 background() 的前后各设置一个 padding():

@Composable
fun PaddingPreview() {Column(Modifier.background(Color.Red)) {Text("Jetpack Compose",Modifier.padding(10.dp).background(Color.Blue).padding(10.dp))}
}

效果如下:

在这里插入图片描述

通过这个示例,我们能看出 Modifier 的特点:对调用顺序敏感,多次调用同一个 Modifier 函数不会覆盖,而是依次调用,呈现多个效果。

至于为什么改变 padding() 的调用位置就把内边距变成外边距的原因,这涉及到 Modifier 的原理,包括 Modifier 的解析与底层存储逻辑原理、测量与布局原理、绘制原理,后面在 Modifier 专题中会详解这些内容。目前我们只需记住 Modifier 对顺序敏感,且多个 Modifier 相同的函数不会覆盖即可。

2.2 Modifier 的常用知识

下面再举一些常见的 Modifier 使用示例,来加深对 Modifier 的理解。

我们先来看尺寸相关的内容。Compose 组件的默认大小遵循 WrapContent 模式,通过上一节例子中的 Column 与 Text 相信你也能看到,我们没有显式指定它们的尺寸,它们就默认呈现了 WrapContent 只包含组件内容的大小。如果想显式设置组件尺寸,可以通过 Modifier 的 size()、width()、height() 等函数指定,如果想呈现 MatchParent 的效果,可以使用 fillMaxHeight()、fillMaxWidth()、fillMaxSize() 等函数指定:

@Composable
fun ModifierPreview() {Column(Modifier.background(Color.Red).fillMaxWidth().height(60.dp)) {Box(Modifier.background(Color.Green).size(50.dp))}
}

效果如下:

在这里插入图片描述

使用 clip() 可以把组件裁剪成指定的形状:

@Composable
fun ModifierPreview() {Column {Image(painter = painterResource(id = R.drawable.ic_launcher_foreground),contentDescription = "null",modifier = Modifier.background(Color.Green))Image(painter = painterResource(id = R.drawable.ic_launcher_foreground),contentDescription = "null",modifier = Modifier.clip(CircleShape) // clip() 要放在 background() 的前面才有效.background(Color.Green))}
}

效果如下:

在这里插入图片描述

最后来说一说如何区分组件上的属性与 Modifier 函数设置的属性,以 Text 组件为例:

@Composable
fun Text(text: String,modifier: Modifier = Modifier,color: Color = Color.Unspecified, // 文字颜色fontSize: TextUnit = TextUnit.Unspecified, // 字体大小fontStyle: FontStyle? = null,fontWeight: FontWeight? = null,fontFamily: FontFamily? = null,letterSpacing: TextUnit = TextUnit.Unspecified,textDecoration: TextDecoration? = null,textAlign: TextAlign? = null,lineHeight: TextUnit = TextUnit.Unspecified,overflow: TextOverflow = TextOverflow.Clip,softWrap: Boolean = true,maxLines: Int = Int.MAX_VALUE,onTextLayout: (TextLayoutResult) -> Unit = {},style: TextStyle = LocalTextStyle.current
)

Text 可以通过参数设置 Text 的一些属性,而 Modifier 也可以设置众多属性,那什么时候用组件参数,什么时候用 Modifier 呢?

Modifier 负责通用设置,也就是很多组件都会有,但与特定组件的特定属性无关的设置,比如组件的尺寸、背景、偏移位置等等。而 Composable 组件函数的参数通常就是针对这个组件的设置,比如 Text 的 color 是指文字的颜色,而不是背景色,fontSize 指定字体大小,在没有文字要展示的组件函数中就不会有这个参数。

这中间有一个特例就是 Button 与 Modifier.clickable()。所有添加了 Modifier.clickable() 的组件都会变成一个可点击的组件,接收并响应点击事件,如:

@Composable
fun ModifierPreview() {Column {Box(Modifier.padding(10.dp).background(Color.Green).padding(10.dp).size(50.dp))Box(Modifier.padding(10.dp).background(Color.Magenta)// clickable() 如果放在第一个 padding() 之前,会导致外边距也在点击范围内,放在// 第二个 padding 之后会造成内边距不在点击范围内,因此只能放在两个 padding() 之间.clickable { println("点击了 Box") }.padding(10.dp).size(50.dp))}
}

上方绿色的 Box 由于没有添加 clickable(),因此不会响应用户的点击事件。而下方的紫红色 Box 添加了 clickable(),在用户点击时会响应点击事件并执行它的尾随 lambda 输出 log。通过点击效果也能看出来:

请添加图片描述

正常情况下,所有组件都会因为设置了通用的 Modifier.clickable() 而具备响应点击事件的能力,但 Button 则是通过它的 onClick 参数来响应点击事件:

@Composable
fun Button(onClick: () -> Unit, // 响应点击事件modifier: Modifier = Modifier,enabled: Boolean = true,interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },elevation: ButtonElevation? = ButtonDefaults.elevation(),shape: Shape = MaterialTheme.shapes.small,border: BorderStroke? = null,colors: ButtonColors = ButtonDefaults.buttonColors(),contentPadding: PaddingValues = ButtonDefaults.ContentPadding,content: @Composable RowScope.() -> Unit
)

当然了,这只是一个表面现象,Button 的底层代码中,还是把这个 onClick 参数传给了 Modifier.clickable() 的 onClick 参数。这样设计是为了提升 Button 这个专门用于被点击的组件的易用性。

3、Compose 的架构分层

传统 View 体系没有分层导致后期扩展困难,比如 ListView 中的回收复用机制就无法被 RecyclerView 使用。早期 View 系统因为赶工没有时间做系统分层,慢慢的导致技术栈越来越多。因此在 Compose 伊始,痛定思痛的开发团队决定要进行明确的分包,这对持续的迭代更新而言是特别方便的。

Compose 分成 androidx 的 7 个 Maven 组 ID:

  • compose.compiler:Kotlin 编译插件,负责转换可组合函数并启用优化功能。在 gradle 中配置 composeOptions 时为 kotlinCompilerExtensionVersion 赋值即可,没有具体的依赖项。严格来讲,它不是这个分层体系中的一环
  • compose.runtime:位于最底层,包含 Compose 的编程模型和状态管理。可以通过 UI 树的 diff 驱动界面刷新,提供基本的对 UI 树的管理能力,如果只需要这个管理能力而不需要 UI,就可以直接基于此层进行构建。state 和 remember 都是这一层的
  • compose.ui:Compose UI 用于与设备进行交互的基础组件,包含 layout、drawing 和 input,它们是上层 Composable 运行的基础,提供了 Composable 的测量、布局、绘制、事件处理以及 Modifier 管理等功能
  • compose.animation:依赖 UI 构建动画提升用户体验
  • compose.foundation:提供相对完整、可用的 UI 体系,比如 Column、Row、Image 等等(但不包含 Button,它在 material 包中),并且这些基础 Composable 可以在多平台通用。此外还提供了特定的手势识别
  • compose.material & compose.material3:Button 在 material 包中。虽然 Button 不是 Material 风格自带的组件,但是 Material 对其风格进行了重新定义,因此也就放在了 material 包中了。此外,像 Floating Action Button、组件具有高度、点击按钮时的波纹效果,这些组件与概念都是 material 首创的。按照 material design,按钮中不止文字,也可以放图片,因此 Compose 的 Button 需要通过 Slot API 传入按钮的具体内容,虽然这相对于传统 View 体系中的 Button 而言,创建文字 Button 要麻烦了一些,但是提升了 Button 内容的灵活性。Button 还有两种子类,OutlinedButton 以及 TextButton

官网给出了具备依赖关系的 4 层,依赖其中一个会自动依赖前置依赖:

请添加图片描述

关于依赖关系,还有一些细节要介绍:

    implementation 'androidx.compose.ui:ui'implementation 'androidx.compose.ui:ui-graphics'implementation 'androidx.compose.ui:ui-tooling-preview'debugImplementation 'androidx.compose.ui:ui-tooling'

ui 包下一共有 4 个包,其中 ui-tooling 依赖于 ui-tooling-preview,ui-tooling-preview 又依赖于 ui。像 Compose 的预览功能,@Preview 注解这些都是 ui-tooling-preview 提供的;而 ui-tooling 则会提供预览界面的交互与部署到测试机等功能。

此外,如果使用 Material 包下提供的矢量图标,可以使用 material-icons-core 和 material-icons-extended 包,前者提供少量图标,后者提供大量图标,二者提供的矢量图对应 ImageVector:

    implementation 'androidx.compose.material:material'implementation 'androidx.compose.material:material-icons-core'implementation 'androidx.compose.material:material-icons-extended'

core 依赖于 extended,它们依赖于 ui 包,不是依赖于 material,相反,是 material 依赖于 core。

参考资料:

androidx.compose 版本

Jetpack Compose 架构层

《Jetpack Compose 从入门到实战》

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词