什么是 Composable
当我们在写 Compose 函数时,如果没有添加 @Composable 注解,IDE 将会提醒报错:
为什么要加 @Composable 注解才能用 Compose 组件?
其实是因为 在编译过程中这些组件会被 Compose 的编译器插件(Compiler Plugin)添加修改一些参数,调用的时候也是使用被修改参数后的函数。
添加的参数准确的说是一个叫 Composer 类型的参数,因为它是 Composable 的必要参数,通过注解的方式在编译时就加上,开发者也能很方便不用手动加上。
这种方式就是典型的面向切面编程(AOP)。在我们熟知的 AOP 方案主要有两种:
-
Annotation Processor 注解处理器
-
修改字节码比如 ASM
而 Compose 用的是更彻底的一种方式,通过编译器插件的方式直接干预编译过程,这样做的好处主要有几点:
-
直接干预编译过程能做更多的事情,功能也比注解处理器这些强
-
Compose 是跨平台的,注解处理器和修改字节码的方案只能用在 JVM,无法用于跨平台
怎么 让编译器插件准确的识别到哪些函数是 Compose 组件的函数,就是用的 @Composable 注解,@Composable 就是识别符的作用。
所以对于为什么要加上 @Composable 注解的原因,就是 Compose 编译器插件要识别到注解的函数,从而在编译期间修改添加上参数,规定作为 Compose 组件的函数以及调用了 Compose 组件函数的函数都要加上 @Composable 注解。
那或许又有疑问了:这些组件函数都要加上 @Composable 注解,调用到的地方也要加上,但总会有一个函数最终是要调用到的,而且没有加注解,是哪一个呢?
最终它会在 Composer.kt 的 invokeComposable() 函数调用:
// 第一个参数是 Composer
internal fun invokeComposable(composer: Composer, composable: @Composable () -> Unit) {
@Suppress("UNCHECKED_CAST")
// 直接进行强转
val realFn = composable as Function2<Composer, Int, Unit>
realFn(composer, 1)
}
可以看到上面非常简单的直接将函数类型参数 composable 做强制转型,这样其实是可以的。
Compose 之所以限制我们在非 Composable 函数里调用 Composable 函数,主要是它没办法在编译过程中自动的识别到,从而就不能自动的加上额外的参数,最终导致程序没法正常运行。
也可以看到 invokeComposable() 函数是加上了 Composer 类型的参数的,是官方规定可以调用的函数,所以可以这么用。
Compose 的做法类似于 suspend 关键字都是通过编译器,但不同的地方是,suspend 是加进 Kotlin 编译器里面,而 Composable 用的外挂的方式实现的。至于它为什么没有演变成像 suspend 加到编译器里面,只是因为 Compose 不是 Kotlin 语言的功能,而是一个基于 Kotlin 的 UI 框架,所以不适合加到 Kotlin 编译器里面。
为了能够更好的表示 “加了 @Composable 注解的函数“,Google 将这类函数叫做 Composable,或者 Composable 函数。
自定义 Composable
简单理解自定义 Composable 就是自己写 Composable 函数,一般我们写 Composable 函数开头字母都是大写,这样能更方便我们开发时一眼就能看出来它是一个 Composable。
或许你会认为能自定义 Composable 函数,那就可以随意写了,例如这样:
class MainActivity : ComponentActivity {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Column {
CustomComposable()
}
Row {
CustomComposable()
}
Box {
CustomComposable()
}
}
}
}
// CustomComposable 这个 Composable 不可掌控不实用,会被外部 Composable 调整
@Composable
private fun CustomComposable() {
Text("name")
Image(painterResource(R.drawable.ic_launcher_background), "Avatar")
}
虽然按上面的写法不会报错,但是这样的 Composable 是很不实用的,Composable 的位置不可掌控,这就类似传统 UI 使用 merge 标签:
// 由外部决定摆放位置,一般开发基本不会用到这种处理方式
<merge >
<ImageView />
<Button />
</merge>
正确自定义 Composable 的方式是,在一个 Composable 函数内只能调用一个别的 Composable,这样是为了方便状态控制等处理。
@Composable
private fun CustomComposable() {
// 只有一个 Column,方便状态控制
Column {
Text("text")
Image(painterResource(R.drawable.ic_launcher_background), "Avatar")
}
}
那什么场景该使用自定义 Composable?和传统 UI 相比是自定义 View 吗?
class CustomTestView(context: Context?, attrs: Attrubites) : LinearLayout(context, attrs) {
private val icon by lazy { ImageView(context) }
private val text by lazy { TextView(context) }
init {
orientation = VERTICAL
...
addView(icon)
addView(text)
}
}
你认为我们写布局的时候会像上面的方式那么写吗?自定义 View 里面再创建子控件和设置属性等等。你确实可以这么写,不过我们更习惯的方式是用 xml 来写上面的布局吧:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemes.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="name" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
对比可以发现,自定义 Composable 的写法更像是传统 UI 中 xml 的写法。那自定义 Composable 就是 xml 写法?也不是。我们把自定义 Composable 再改改:
class MainActivity : ComponentActivity {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Box {
CustomComposable("Android")
CustomComposable("kotlin")
}
}
}
}
@Composable
private fun CustomComposable(name: String) {
// name 如果相同,remember 内的代码块不会重复执行直接返回上次的结果,性能比较好
val calculatedName = remember(name) {
name + if (name.length > 5) {
": long name"
} else {
": short name"
}
}
Column {
Text(calculatedName)
Image(painterResource(R.drawable.ic_launcher_background), "Avatar")
}
}
现在再看看上面的 Composable 里面加了逻辑,xml 能加逻辑吗?其实是不能的,在 xml 不能实现任何逻辑处理的,它是一个标记语言,所以是没法实现想自定义 Composable 能处理逻辑的效果的。
实际上 自定义 Composable 相比于传统 UI 的等价物就是 xml 布局 + 自定义 View,xml 只能布局简单直观,而自定义 View 是能处理逻辑的,所以二者的结合才是自定义 Composable。
所以 自定义 Composable 的使用场景就是遇到任何需要对界面进行定制的需求,我们就可以使用它。当然还有绘制、布局、触摸反馈这类高级定制。
总结
什么是 Composable?
Composable 就是带 @Composable 注解的函数,@Composable 注解是用于编译器识别。
-
所有加了 @Composable 注解的函数,都需要在别的加了 @Composable 的函数里面才能调用
-
所有内部没调用任何其他 @Composable 函数的函数,都没必要加上这个注解,因为没意义
除了有特殊需求,否则一个 Composable 里面只能调用一个别的 Composable,这样是为了方便状态控制等处理。
什么时候自定义 Composable?
自定义 Composable 和传统的写法相比,类似于 xml 布局 + 自定义 View 的方式(自定义 View 并不是一定要自定义测量布局绘制,也可以是添加基础逻辑处理)。