Jetpack Compose -> 分包 & 自定义Composable

前言

上一章我们讲解了 Compose 基础UI 和 Modifier 关键字,本章主要讲解 Compose 分包以及自定义 Composable;

Compose 如何分包

我们在使用 Button 控件的时候,发现如果我们想给按钮设置文本的时候,Button 函数并没有直接提供设置 text 的参数,要我们自己去调用 Text 进行设置;

ini 复制代码
Column {    
    Button(onClick = {}) {        
        Text(text = "我是老A")    
    }
}

可能到这里的时候,大家就会困惑了,Compose 为什么要这么搞呢?我们可以去源码中一探究竟,我们可以看到 Button 函数是在 androidx.compose.material3 这个包下面

Button 来自 compose.material3 这个组下面的,也就是 Maven 包的 groupId 是 androidx.compose.material3,对应的就是 build.gradle 中的依赖关系

其实Compose 由 androidx 中的 7 个 Maven 组 ID 构成。每个组都包含一套特定用途的功能,并各有专属的版本说明;

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 下的

scss 复制代码
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 下的

less 复制代码
@Preview
@Composable
fun preview() {    
    Column {        
        Button(onClick = {}) {            
            Text(text = "我是老A")        
        }        
        OutlinedButton(onClick = { /*TODO*/ }) {            
            Text(text = "我是老A")        
        }        
        TextButton(onClick = { /*TODO*/ }) {            
            Text(text = "我是老A")        
        }    
    }
}

还有 material3 提供的一些矢量图组件

scss 复制代码
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 的实现:

kotlin 复制代码
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 函数,这样方便我们对于布局的控制

kotlin 复制代码
@Composable
fun ui() {
    Column {
        Text("老A")
        Text("Mars")
    }
}

而不是

kotlin 复制代码
@Composable
fun ui1() {
    Text("老A")
    Text("Mars")
}

那么外部在调用 ui1 函数的时候,我们的布局就不受控制了,如果外部调用的时候 放到了 Column 中,那么就会竖向排列,如果放到了 Row 中,就会横向排列,如果放到了 Box 中就会叠加排列;

而 ui 函数我们可以自己控制布局的排列,通过 Column、Row 等函数,而不用受外界调用控制;

自定义 Composable 的应用场景

再说使用场景的时候,我们可以先想领一个问题,自定义 Composable 在传统 View 中的等价物是什么?自定义View?还是 xml 文件?还是 自定义View + xml 文件?

自定义View?

kotlin 复制代码
@Composable    
fun ui() {
    Column {            
        Text(text = "老A")            
        Text(text = "Mars")
    }
}

这种写法,看起来更像传统的 自定义 LinearLayout

scss 复制代码
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 中进行了声明

ini 复制代码
<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 函数做如下改动使用:

kotlin 复制代码
@Composable
fun ui(name: String) {    
    Column {        
        Text(text = name)        
        Text(text = "Mars")
    }
}

我们设置了一个 name 作为参数来传入进来,那么我们就可以在调用的时候传入不同的值,来表现不同的数据,而且,这个 Composable 函数还可以这么改

kotlin 复制代码
@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 详解;

欢迎三连

来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~

相关推荐
小蜜蜂嗡嗡1 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi001 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil3 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
你过来啊你3 小时前
Android View的绘制原理详解
android
移动开发者1号6 小时前
使用 Android App Bundle 极致压缩应用体积
android·kotlin
移动开发者1号6 小时前
构建高可用线上性能监控体系:从原理到实战
android·kotlin
ii_best11 小时前
按键精灵支持安卓14、15系统,兼容64位环境开发辅助工具
android
美狐美颜sdk11 小时前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk
恋猫de小郭15 小时前
Meta 宣布加入 Kotlin 基金会,将为 Kotlin 和 Android 生态提供全新支持
android·开发语言·ios·kotlin
aqi0016 小时前
FFmpeg开发笔记(七十七)Android的开源音视频剪辑框架RxFFmpeg
android·ffmpeg·音视频·流媒体