前言
上一章我们讲解了 Compose 基础UI 和 Modifier 关键字,本章主要讲解 Compose 分包以及自定义 Composable;
Compose 如何分包
我们在使用 Button 控件的时候,发现如果我们想给按钮设置文本的时候,Button 函数并没有直接提供设置 text 的参数,要我们自己去调用 Text 进行设置;
Column { Button(onClick = {}) { Text(text = "我是老A") } }
可能到这里的时候,大家就会困惑了,Compose 为什么要这么搞呢?我们可以去源码中一探究竟,我们可以看到 Button 函数是在 androidx.compose.material3 这个包下面
Button 来自 compose.material3 这个组下面的,也就是 Maven 包的 groupId 是 androidx.compose.material3,对应的就是 build.gradle 中的依赖关系
其实Compose 由
Compose 其实一共是分了6层,material 和 material3 是一个,只是不同的分支;每个组下面有不同的分包,我们其实可以看到 ui 下面就有不同的ui、ui-tooling-preview、ui-graphics 等等,Android 团队这么分包,其实是针对 View 系统的一个优化;
View 系统是没有这个分层的,这就导致后期越来越严重的扩展性问题,例如 View 系统中的 ListView,ListView 中有一个对 View 的回收复用机制,这个机制 RecyclerView 是没有办法复用的,也就是它们两个各自维护着一套复用机制,这就是分层不明确导致的;
所以 Compose 在设计之初就明确了分层概念,分层之后的各自扩展,就不会受到限制;
compose.compiler 严格来说,它其实并不属于这7层,它提供的并不是库依赖,它代表的是 kotlin 编译插件,转化 @Composable functions 并启用优化功能,它是负责编译过程的,我们在依赖里面也完全不需要去配置它,只需要在 Compose 的专用配置地方去写上你要的编译插件版本就行,对应的就是这里:
Compose 剩下的 Group 都是我们开发 Compose 的时候会用到的,不过它们有依次递进的依赖关系;
最下层是 compose.runtime 它包含了 Compose 编程模型和状态管理的基本构件块,以及 Compose 编译器插件的目标核心运行时,是最底层的概念模型,比如用来保存状态的 State 就在 compose.runtime,还有 mutableStateOf、remember
往上一层是 compose.ui 它是用来提供 ui 最基础的功能,比如绘制、测量、布局、触摸反馈等最底层的支持,比如我们使用的所有控件函数,最终都会调用到一个叫 Layout 的函数,这个函数就在 ui 这层;
再往上一层是 compose.animation,它是用来构建动画的;
在往上一层是 compose.foundation,它提供的是一套相对完整可靠的 UI 体系,例如 Colum、Row、Image 等都在这一层;
再往上一层就是 comose.material/material3 了,这是一个封装了 一堆 material design 风格控件的包,如果不想使用 MD 风格,可以使用 foundation 层自己组装一套风格出来;
接下来就是同一个组下面的多个包应该如何引用?例如 compose.ui 下的
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") androidTestImplementation("androidx.compose.ui:ui-test-junit4")
一般来说,我们只需要引入和组名相同的包就可以了,因为一般这个包就包含了这个组下其他包的所有依赖,除了测试组的这种,例如 compose.ui:ui 下不会包含 compose.ui:ui-test-xxx 和 compose.ui:ui-tooling 因为 test 和 工具类的一般都不会编译进我们的 apk 中;
例如 @Preview 就属于 ui-tooling 下的
@Preview @Composable fun preview() { Column { Button(onClick = {}) { Text(text = "我是老A") } OutlinedButton(onClick = { /*TODO*/ }) { Text(text = "我是老A") } TextButton(onClick = { /*TODO*/ }) { Text(text = "我是老A") } } }
还有 material3 提供的一些矢量图组件
implementation("androidx.compose.material3:material3-icon-extends") implementation("androidx.compose.material3:material3-icon-core")
也是需要单独依赖的;
compose.ui:ui 一般包含了 ui 下的所有, compose.material3:material3 一般包含了 material 下的所有;
自定义Composable
用自定义函数的方式来写 Composable,而 Composable 是一种简化的方式,它指的是带有这个 Composable 注解的函数,那么这个注解到底是做什么的呢?我们来一探究竟
我们在使用的 Text 函数、Image 函数等其实都带有 Composable 注解,但是这些函数并不是原封不动的被调用的,而是会在编译过程中被动了手脚,给它们增加了一些函数参数,然后在运行的时候,调用的其实是那些被改过的参数更多的版本,比如说它们被加入的其中一个参数就是 Composer 类型的,总之这些 Composable 函数在编译的时候会被 Compose 的编译器插件(Compiler Plugin)修改,添加一些参数,运行的时候也是调用的这些被修改过的函数;
那么,编译器为什么要修改它们呢?
最重要的一点就是:要在代码中增加一些我们没有写出来的功能,这些功能对于开发者来说不需要,只需要在程序运行的时候能用到就可以了,所以编译的时候添加,即方便了开发者,又不影响程序的运行;
这其实也是一种面向切面(AOP)编程的思想;
那么编译器插件又是怎么认出这些函数的呢?它怎么直到哪些应该被修改呢?
靠的就是 @Composable 注解;只有被加了这个注解的才会进行修改,起到了识别符的作用;我们可以来看一个小例子:
如果 ui 函数没有添加 @Composable 注解,编译器直接报错了,就是因为这个函数内部调用了被 @Composable 注解的函数,所以我们可以理解为:所有调用了被 @Composable 注解的函数的函数,也必须添加上 @Composable 注解;说到这里的时候,可能会有人有疑问了,setContent 函数添加了 @Composable 注解了吗?如果没有添加,那么它内部怎么可以调用 Compose 函数?如果添加了,那么 MainActivity 为什么不用添加 @Composable 注解?我们来看看 setContent 的实现:
public fun ComponentActivity.setContent( parent: CompositionContext? = null, content: @Composable () -> Unit) { }
我们发现,setContent 函数并没有被 @Composable 注解标记,它只是把一个 @Composable 注解的函数作为了参数,所以 setContent 不需要被其注解;但是终归还是需要一个被 @Composeable 注解的函数来调用这个参数,那么这个函数是哪个函数呢?它就是 invokeComposable 函数
默认看不了,我们 Decompile to Java 看下
就是将 composable 强转成了一个 Function2 函数,然后进行调用;
所以自定义 Composable 就是声明的函数被 Composable 注解标记,本质上就是为了方便我们在开发中可以将我们的界面元素进行拆分,从而实现不同的功能;通常我们在自定义 Composable 的时候,直接的只会调用一个 Composable 函数,这样方便我们对于布局的控制
@Composable fun ui() { Column { Text("老A") Text("Mars") } }
而不是
@Composable fun ui1() { Text("老A") Text("Mars") }
那么外部在调用 ui1 函数的时候,我们的布局就不受控制了,如果外部调用的时候 放到了 Column 中,那么就会竖向排列,如果放到了 Row 中,就会横向排列,如果放到了 Box 中就会叠加排列;
而 ui 函数我们可以自己控制布局的排列,通过 Column、Row 等函数,而不用受外界调用控制;
自定义 Composable 的应用场景
再说使用场景的时候,我们可以先想领一个问题,自定义 Composable 在传统 View 中的等价物是什么?自定义View?还是 xml 文件?还是 自定义View + xml 文件?
自定义View?
@Composable fun ui() { Column { Text(text = "老A") Text(text = "Mars") } }
这种写法,看起来更像传统的 自定义 LinearLayout
class CustomLinearLayout(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) { val name: TextView by lazy { TextView(context) } val alias: TextView by lazy { TextView(context) } init { orientation = VERTICAL // name.text = "老A" alias.text = "Mars" ... // 省略部分代码 addView(name) addView(alias) } }
看起来更像是 自定义 View 的等价物;
xml文件?
但是,这种简易布局我们一般也不会这样去使用,通常都是直接在 xml 中进行了声明
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>
这样更直观,便捷,看起来也更像 compose 的写法,一个父控件,两个子控件;
自定义View + xml?
但是如果我们对 Composable 函数做如下改动使用:
@Composable fun ui(name: String) { Column { Text(text = name) Text(text = "Mars") } }
我们设置了一个 name 作为参数来传入进来,那么我们就可以在调用的时候传入不同的值,来表现不同的数据,而且,这个 Composable 函数还可以这么改
@Composable fun ui(name: String) { Column { val realName = remember { if (name.length > 8) { "我是laoA" } else { "我是马尔斯" } } Text(text = realName) Text(text = "Mars") } }
对于 Compose 可以这么写,但是对于传统的 xml 实现不了,一旦我们对界面有了定制的需求后,就只能通过自定义 View 来实现了;
所以,看起来自定义 Composable 更像传统 View 的自定义 View + xml 文件!
所以自定义 Composable 的使用场景也就能知道了;
界面声明我们一般是一个 Activity 对应一个 xml 的文件,那么当我们使用 Compose 的时候,也可以一个 MainActivity 对应一个 MainLayout 的 Composable 的函数;
当我们既需要 xml 的简洁有需要自定义view的逻辑处理能力,那么都是可以使用自定义 Composable 的;遇到任务需要对界面有定制需求,就直接使用 Composable 函数处理;
传统自定义 View 还能对布局、绘制、触摸反馈进行定制,这一类的高级自定义 View 在 Compose 中是怎么实现的呢?
其实还是用的自定义 Composable,当然如果你不自定义 Composable,直接硬写也是可以的,但是就失去了扩展、复用的能力,具体写法上,大部分用的是 Modifier,后面章节会详解自定义 Compose 中的高级自定义 View;
好了,自定义 Composable 就讲到这里吧~~
下一章预告
MutableState 和 mutableStateOf 详解;
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~