Jetpack Compose -> 分包 & 自定义Composable

前言

 上一章我们讲解了 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 由 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 下的

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 详解;

欢迎三连

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