对于Android开发,Jetpack Compose真的要开始学起来了?

10,361 阅读8分钟

Jetpack Compose 是个啥?为啥要学它?

谷歌对 Jetpack Compose 的定义:

Jetpack Compose 是推荐用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速打造生动而精彩的应用。

提取关键词:界面开发新工具包、简化并加快界面开发、Kotlin API

对于大部分Android项目来说,如果基础库(如网络库、hybird、图片加载、热修复库等)已经搭好,那么平时大部分时间就是跟 UI界面、需求逻辑 打交道了,而谷歌提供的 Jetpack Compose 正好是加快界面开发的工具包对比

就跟魂斗罗里的子弹类型似的,使用普通子弹(XML方式)也可以通关,但是相比之下耗时更长;而换成超级子弹(Jetpack Compose)体验就不一样了,耗时更少,而且游戏体验更爽!

命令式UI vs 声明式UI

长期以来,Android 视图层次结构一直可以表示为界面 widget 树。最常见的界面更新方式是使用 findViewById() 等函数遍历树,并通过调用 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法更改节点。这些方法会改变 widget 的内部状态,这种手动更新UI的方式即是命令式UI

在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程任务。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 即是一个声明式UI框架。

Jetpack Compose要学起来了?

很遗憾,Jetpack Compose 确实要学起来了(快起来,你还能学!哈哈...),随着Jetpack Compose 版本的不断迭代,API 逐渐稳定了,性能也越来越好了。

优点

  • 更少的代码:编写代码只需要采用Kotlin,而不用拆分成 Kotlin + XML方式了。
  • 直观:只需描述界面,Compose会负责处理剩余工作。应用状态变化时,界面自动更新。
  • 加速开发View 与 Compose 之间可以相互调用,兼容现有的所有代码。借助AS可以实时预览界面,轻松执行界面检查。
  • 功能强大:直接访问Android API,内置对Material Design、主题、动画等的支持。

Jetpack Compose vs Flutter

  • Jetpack Compose的目的是为了提高 Android 原生的 UI 开发效率!声明式UI已经成为主流的开发方式了,就像当初谷歌将Kotlin定为Android主流语言时我们学习Kotlin一样,未来Jetpack Compose 一定会是Android UI开发的主流方式。
  • Flutter 的定位是多平台 UI 框架,优势在于跨平台。

大家很喜欢把Jetpack Compose 和 Flutter作对比,不知道该学哪一个?的确,某些场景下它们确实挺像的,而且还都是谷歌在推的。

个人理解是:如果你未来的主攻方向还是Android,那么无脑选择Jetpack Compose,虽然Compose目前也能实现跨端,但跨端目前看并不是它的主要工作;而如果你的方向是多平台开发,那么学习Flutter是首选吧

另外,与其一直纠结学哪一个,不如直接上手亲身感受下它们的不同,正所谓 “纸上得来终觉浅,绝知此事要躬行”。

Jetpack Compose入门

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello World!")
        }
    }
}

其中,setContent()传入一个@Composable作用域,其作用跟之前的setContentView()一样用来设置界面。Text()用来描述一个UI元素,里面有各种参数,这里我们只把文案填上去,执行结果:

hello world

一个最简单的功能就完成了。

1、@Composable 可组合函数

还是上述展示一个文本的功能,我们换一种写法:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MessageCard("Android")
        }
    }
}

@Composable
fun MessageCard(name: String) {
    Text(text = "Hello $name!")
}

执行效果跟上面一样。唯一的区别就是把文本展示单独抽离到一个方法中了,并且该方法上面加了@Composable 注解

@Composable注解用于标记一个函数为可组合函数。可组合函数是一种特殊的函数,不需要返回任何UI元素,因为可组合函数描述的是所需的屏幕状态,而不是构造界面widget;而如果按我们以前的XML编程方式,必须在方法中返回UI元素才能使用它(如返回View类型)。

@Composable注解的函数之间可以相互调用,因为这样Compose框架才能正确处理依赖关系。另外,@Composable函数中也可以调用普通函数,而普通函数中却不能直接调用@Composable函数。 这里可以类比下kotlin中suspend挂起函数的用法,其用法是相似的

几个定义:

  • 组合:对 Jetpack Compose 在执行可组合项时所构建界面的描述。
  • 初始组合:通过首次运行可组合项创建组合。
  • 重组在数据发生变化时重新运行可组合项以更新组合

可组合函数的特点:

  • 使用同一参数多次调用此函数时,它的行为方式相同,并且它不使用其他值,如全局变量或对 random() 的调用。
  • 此函数描述界面而没有任何副作用,如修改属性或全局变量、点击事件的处理等。当需要执行附带效应时,应通过回调触发。如:
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

可见通过回调,点击事件这个附带效应是在调用方触发的。

在编译时,Jetpack Compose会将标记为@Composable的函数编译成字节码,并生成一个专门的ComposeNode类来管理其状态和属性。这个类会自动处理依赖关系,并在需要时计算UI元素。这样,开发者就可以专注于编写UI逻辑,而不用担心状态管理和UI更新的细节

这里引出一个问题,Compose 是如何做 UI 更新的呢?总不能每次有一小部分数据的变化,整个UI都要跟着刷新一次吧,那性能肯定差的要死。其实,当有数据变化时,Compose实现的是增量更新,只会重新绘制数据有改动的UI(该过程称为重组),数据没有改动的则不会重新绘制了

2、布局基础知识

布局 Compose 通过元素组合、布局、绘制之后可以将状态转换为UI元素。

组合

在 Compose 中,可以通过从可组合函数中调用其他可组合函数来构建界面层次结构。 基础布局 如图所示:

  • Column :可以将多个项垂直地放置在屏幕上;
  • Row :可以将多个项水平地放置在屏幕上;
  • Box :可将元素放在其他元素上,还支持为其包含的元素配置特定的对齐方式。

排列及对齐方式:

/**
 * @param verticalArrangement 竖直排列方式
 * @param horizontalAlignment 水平对齐方式
 */
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {...}

/**
 * @param horizontalArrangement 水平排列方式
 * @param verticalAlignment 竖直对齐方式
 */
@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
) {...}

/**
 * @param contentAlignment 内容对齐方式
 */
@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
) {...}

verticalArrangement、horizontalArrangement 排列方式及效果: 排列方式

布局

界面树布局通过单次传递即可完成。父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整Layout流程 当界面树较深时,Compose 可以通过只测量一次子项来实现高性能。

3、Modifier修饰符

可以通过Modifier修饰符更改可组合项的大小、布局、外观,还可以添加高级互动,例如使元素可点击等。如:

  Image(
    painter = painterResource(id = R.mipmap.icon_water_melon),
    contentDescription = "",
    modifier = Modifier
         .size(50.dp)
         .clip(CircleShape)
         .border(width = 2.dp, Color.Red, CircleShape)
   )

执行结果:Modifier,原图本身是长方形的,通过Modifier修饰符修饰后,很容易变成圆角图片。想一下如果用XML方式来写,是不是要写好多代码呢。

4、存储状态

可组合函数中可以使用 remember 将本地状态存储在内存中,并跟踪传递给 mutableStateOf 的值的变化。该值更新时,系统会自动重新绘制使用此状态的可组合项(及其子项),这也是上面所说的重组。如:

@Composable
fun MessageCard(msg: Message) {
  // We keep track if the message is expanded or not in this variable
  var isExpanded by remember { mutableStateOf(false) }
  // We toggle the isExpanded variable when we click on this Column
  Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {...}
}

当点击Column 元素时,每次都会重新执行MessageCard()可组合函数进行刷新,而通过remember和mutableStateOf可以保存了上次的isExpanded状态;如果不使用它们,重新执行 MessageCard() 时 isExpanded 也会重新初始化。

除了remember之外,还有rememberSaveable、savedStateHandle.saveable等。。。

总结

这篇文章主要讲了Compose是什么以及我们要开始学习它的必要性。作为Compose 第一篇介绍文章,本文旨在初步感受一下 Compose的能力,后续再详细研究 Compose 的精彩用法!

资料

【1】谷歌Jetpack Compose 教程https://developer.android.com/jetpack/compose/tutorial?hl=zh-cn