☰
Compose入门:创建第一个Android声明式UI项目完全指南
2026/10/10 10:33:38 网站建设 项目流程

1. Compose项目的核心认知:它改变的到底是什么

先快速对齐一个概念:所谓第一个Compose项目,不是多学一个框架的问题,而是在改变Android UI开发的底层思维。传统View体系下,界面长什么样由XML决定,界面什么时候更新、怎么更新,由开发者手动控制:findViewById、setText、addOnClickListener、notifyDataSetChanged,一环扣一环,页面状态一多就容易失控。

Compose把这一切倒了过来。界面不再是一份静态布局文件,而是一个个用Kotlin函数描述的可组合函数。函数接收什么数据,就画出什么界面。数据变了,函数用新的数据重新执行一遍,界面自动更新。这种模式叫声明式UI,核心逻辑只有一句话:数据就是真相,状态驱动界面,没有中间商。

适合什么样的人看这篇内容?两种人收获最大。一种是有Android基础、但还没系统性学过Compose的开发者,熟悉这套思路后基本上能直接判断手里老项目的改造路径。另一种是刚接触Kotlin、想直接从新时代UI玩法入门的新手,跳过了XML时代的历史包袱,反而少了很多负担。

而如果你是纯Web前端转过来的,这个理解起来就更快,Compose的思想跟React、Vue那套"状态到视图"的映射模型异曲同工,只是底层跑在Android平台的渲染引擎上。

在开始动手之前,还有一个认知值得提前建立:Compose不是单指某个控件库,而是一整套技术栈。它包含了一套UI框架、一套状态管理机制、一套与系统生命周期协作的配套工具,以及围绕编译期处理和实时预览的构建设施。这就意味着,你搭建的"第一个Compose项目",从目录结构、依赖声明到运行逻辑,都会跟原来的View体系项目有明显区别。别用老思维去套,先把自己清空,再往下看。

2. 创建第一个Compose项目的完整实操

2.1 环境准备:版本匹配是第一道坎

创建Compose项目本身不难,难在最基础的环境环节就已经埋了三四五六七八个坑。

先检查IDE版本。当前主流的稳定版本,新版本自带完整的Compose支持,也能自动识别项目类型和模板。如果你的IDE还是老版本,先升级到较新版本再谈Compose。这一点可以直接在IDE的"检查更新"里完成。版本太老不是不能写Compose,而是模板、编译器插件、预览工具之间的兼容性很难受,光是处理各种版本警告就够喝一壶。

接着看Gradle版本和Android Gradle Plugin版本的匹配关系。这一步很多新手不重视,直接把默认配置一路回车,结果构建或者编译插件时爆出Download异常,或者Kotlin编译器和Compose编译器版本冲突,进程直接带崩。

这里有个很实际的建议:不追求最新,追求稳定组合。在配置项目时,可以参考当前各组件(Kotlin、CompileSdk、TargetSdk、APG)的适配表,选一组经过打磨的组合。比如当前比较通用的一套是Kotlin 1.9.x + Compose编译器插件1.5.x + CompileSdk 34。不用盲目升级到Kotlin 2.x加Compose编译器插件,除非你确实需要新特性,因为那套配置项的工作原理变了,很多旧教程对比不上,容易被误导。

还需要确认JDK版本,JDK 17是用默认配置创建新项目时比较常见的设置。JDK版本若不匹配,Gradle同步时会提示Unsupported class file major version,这类问题其实好解决,点开项目结构和SDK位置,把Gradle JDK改为已安装的JDK版本就行。

2.2 新建项目:模板选择有讲究

打开IDE,选择新建项目,列表里通常会有两个跟Compose相关的选项:一个是"Empty Activity",一个是"Empty Activity (Compose)"。如果创建向导里还区分了"Compose"标签,直接选带Compose字样的那个。

这里说明一下为什么模板选择重要。选择普通空Activity模板生成的项目,默认依赖布局文件,不会自动引入Compose相关插件和依赖。之后你要在里面启用Compose,需要手动去改Gradle脚本配置,新增kotlin编译器插件、加依赖、改buildFeatures,一环没跟上,后面开发体验就很差。而Compose模板会把这些前置配置全部备齐,你直接在代码里写UI就行。

创建的时候,有几个细节顺手设置好,能省后面的麻烦:

  • 项目名称和包名使用小写字母,避免特殊字符,减少不必要的编译警告。
  • Minimum SDK选择API 24以上比较稳妥,这样可以避免为了兼容低版本而额外处理各类兼容逻辑。
  • 语言选择Kotlin,这是Compose的基础,不用想。

等待项目初始化完成后,先看一眼整体目录结构,你会发现res/layout目录下没有activity_main.xml,或者说几乎没有布局文件。这不奇怪,真正的工作区域集中在MainActivity.kt和.ui包里面。

2.3 项目骨架拆解:那些自动生成的文件在做什么

第一次在Compose模板里创建项目时,IDE会帮你生成一组预置文件。这些文件不是摆设,每一处都值得过一遍。

先看根目录的build.gradle.kts,它包含了Compose相关的插件声明。里面会有com.android.application(或者对应新版本里的Android插件)和org.jetbrains.kotlin.plugin.compose。后者很重要,是把Kotlin代码转译成Compose运行时能识别的结构的编译期插件,它的存在是Compose能在Kotlin上顺畅工作的前提。

然后是模块级build.gradle.kts,这里能看到:

  • buildFeatures.compose = true,开关打开后编译器才会处理Composable函数。
  • composeOptions里的kotlinCompilerExtensionVersion,这个版本必须和Compose运行时库版本匹配,很多预览或编译异常都可能从这里来。
  • 一堆compose依赖:ui、material3、tooling-preview、activity-compose等等。这些依赖初期不需要弄得很明白,但要知道每条依赖各管什么。比如material3管的是主题化和控件样式,tooling-preview管的是IDE实时预览,activity-compose提供ComponentActivity与Compose的桥接。

再看MainActivity.kt,正常情况下它是这样一幅面貌:类继承ComponentActivity,onCreate里调用了setContent,setContent的代码块里直接写Composable内容。跟过去setContentView(R.layout.activity_main)完全不一样,不再有布局文件的概念,View层直接由Kotlin来描述。

还有一处容易忽视:ui/theme目录下的三个文件,Color.kt、Theme.kt、Type.kt。这些是Compose主题体系的三件套。Color.kt定义颜色值,Theme.kt定义浅色/深色模式的颜色映射和整体主题,Type.kt定义字体字号样式。虽然第一个项目里你不会马上深入使用它们,但后续项目主题化改造时,一定要知道修改这些文件能全局改变App外观,比在XML里逐层改样式要集中得多。

2.4 跑起来:模拟器里的第一屏

直接点运行,选择一个模拟器设备。初次构建Compose项目会比普通项目慢一些,因为涉及Kotlin编译插件和Compose编译器的工作量比较大,耐心等。

运行成功之后,默认效果是一块纯色背景的屏幕加一行问候语文本。这个效果平淡,但背后机制不简单:界面的每个元素都是通过函数调用组合出来的,没有遍历视图树的逻辑,也没有findViewById的存在。屏幕上呈现的,其实是编译器处理后的一个高度优化的绘制指令流。

在真正开始写之前,建议先在模板代码上做点小实验,体会一下"可组合函数改一改,界面跟着变"的反馈循环。比如把Text里的字符串从"Hello Android"改成你自己的项目名,再改一个Text的文本颜色和字体大小。每次改完,界面都跟着更新。这种同步反馈感就是Compose开发的基本节奏,后面所有复杂界面,本质上都是对这种节奏的放大。

3. 核心概念拆解:从可组合函数到状态驱动

3.1 可组合函数的基本功

Compose世界的最小单位是可组合函数,用@Composable注解标记,函数名通常首字母大写,用来描述一段UI长什么样。

下面这段示例,就是定义一个简单的信息卡片:

@Composable fun InfoCard(name: String, isOnline: Boolean) { Card( modifier = Modifier.padding(8.dp) ) { Column( modifier = Modifier.padding(16.dp) ) { Text( text = name, style = MaterialTheme.typography.titleMedium ) Text( text = if (isOnline) "在线" else "离线", color = if (isOnline) Color(0xFF4CAF50) else Color.Gray ) } } }

这段代码有几个值得留意的点:

  • InfoCard接收了两个普通参数name和isOnline,这就是"数据输入"。函数执行时,参数决定了UI长什么样。
  • 没有返回值的概念。可组合函数不是传统意义上"生成一个视图并返回"的函数,而是向组合结构中发出界面指令。编译器做了很多幕后工作来保证执行效率,你不需要完全弄懂原理也能用,但知道这一点能避免写出很多奇怪代码。
  • Column、Text、Card这些是Compose内置的可组合函数。它们不是控件,而是构建UI结构的"砖块"。可以用Modifier来调整尺寸、间距、背景、点击行为等,这是Compose里一个很核心的链式调用对象。

可组合函数能放在条件分支里、能被for循环反复调用、能被随意封装。这在传统View体系里不可想象。以前要复用一段带样式的视图,要搞include标签、自定义View、适配器或ViewHolder,非常绕。现在就是"写一个函数,到处调用"这么简单。

3.2 状态:界面为什么自己会变

Compose里最核心的一个概念是状态。先看一个简单的计数器例子:

@Composable fun CounterApp() { var count by remember { mutableStateOf(0) } Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "点击次数: $count", style = MaterialTheme.typography.headlineMedium ) Spacer(modifier = Modifier.height(16.dp)) Button( onClick = { count++ } ) { Text("点我") } } }

这里最有价值的一行是loading那个状态声明。拆开看:

  • mutableStateOf(0):创建一个可观察的容器,初始值为0。
  • remember:让这个状态在重组时不被重新初始化,留在组合树上。
  • by委托:用简洁的读写语法把count和状态容器绑定起来。

当点击按钮执行count++时,mutableStateOf内部会通知Compose运行时:"这个状态变了"。运行时找到读取过count的可组合函数,触发一次携带新数据的重新执行,也就是重组。重组后文本内容变成新的数值,界面随之刷新。

这个概念之所以重要,是因为它构成了Compose整个响应式更新的基础。你不需要去操作任何view实例,不需要手动刷新列表,不需要处理数据回调后再塞进控件的逻辑。只要数据变了,界面自动跟着表达最新状态。

如果到这里觉得"跟React的useState有点像",说明你已经抓住了本质。两者思路确实同源。

3.3 重组与性能的理性看待

有一个容易让新手纠结的问题:"每次状态变化都从头重新执行整个函数,会不会很浪费性能?"

答案是,Compose的所谓"重新执行"不是字面意义上重画所有像素。重组机制做了很多优化,编译器会分析哪些代码块读取了变化的状态,只让这部分代码块参与重组,其他部分跳过。同时,绘制环节也和视图树解耦了,重组跟布局、绘制按各自节奏来,不会互相拖累。

作为刚起步的开发者,不必过度优化,但有一个坏习惯建议尽早戒掉:不要在可组合函数里做高开销操作,比如频繁创建大对象、做复杂计算或者执行I/O。因为函数可能在状态变化时被重新调用,这类操作就会反复执行,拖累性能。Compose的做法是,把这类业务逻辑放到ViewModel或普通类里,通过状态对外暴露结果;UI层只做"状态到界面"的翻译工作。

3.4 实战扩展:给启动页加点真实内容

模板项目默认界面比较单调,我们可以自己动手写一个稍微丰富一点的首页,把前面两节说的组件、状态串起来。

设计思路是做一个简单的人物介绍卡片加一个关注按钮,点击关注后按钮文案和颜色变化。这里涉及Boolean状态、状态传递和界面重组,正好覆盖Compose最常见的使用场景:

@Composable fun ProfileScreen() { var followed by remember { mutableStateOf(false) } Scaffold( topBar = { TopAppBar(title = { Text("我的主页") }) } ) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .fillMaxWidth() .padding(16.dp) ) { ProfileCard( userName = "开发者A", userDesc = "专注跨端技术方案", userAvatar = "A" ) Spacer(modifier = Modifier.height(16.dp)) Button( onClick = { followed = !followed }, modifier = Modifier.fillMaxWidth() ) { Text(if (followed) "已关注" else "关注") } } } } @Composable fun ProfileCard(userName: String, userDesc: String, userAvatar: String) { Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .background(MaterialTheme.colorScheme.surfaceVariant) .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .size(48.dp) .clip(CircleShape) .background(MaterialTheme.colorScheme.primary), contentAlignment = Alignment.Center ) { Text( text = userAvatar, color = MaterialTheme.colorScheme.onPrimary, style = MaterialTheme.typography.titleMedium ) } Spacer(modifier = Modifier.width(16.dp)) Column { Text( text = userName, style = MaterialTheme.typography.titleMedium ) Text( text = userDesc, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } }

这段代码还把一个新的知识点带进来了:Scaffold。Scaffold是Material3提供的页面骨架组件,帮你处理顶部栏、底部栏、浮动按钮与正文内容的避让关系。它的innerPadding参数很重要,正文内容要主动设置padding,否则顶部栏可能会遮挡内容。这是很多初学者容易遗漏的细节,效果就是页面显示不完整,以为出了Bug,其实只是padding没处理。

运行这段代码后,点击"关注"按钮,文案会立刻变为"已关注",再点一次又变回去。这个交互背后的链路就是:状态变化、触发重组、读取新状态、重新绘制。你已经亲手完成了一个具备有状态交互的Compose界面。

3.5 主题与资源:让界面不"裸奔"

Material3主题默认提供的配色和字体已经足够体面,但项目终归要有自己的风格。在Compose里调整主题的路径和传统View体系完全不同。

对Color.kt、Theme.kt、Type.kt三个文件做一次彻底的改造,就可以给整个App换皮肤。比如想做一个偏科技感的深色主题,就可以在Color.kt里加一套深色背景、亮色文字的颜色值,然后在Theme.kt的darkColorScheme里替换对应的色组。字体字号则在Type.kt里调整,Typography类用一套可组合的文本样式参数来定义。

这种集中式的主题管理方式,带来的好处是全局一致性和修改效率。一个App几十个页面,如果每页用手写Color(0xFF....)写死颜色,后期改视觉风格就是灾难。走主题化路径,改一处全局生效,分支处理也简单。

4. 避坑指南:第一个Compose项目常见问题排查

4.1 依赖版本冲突

这是项目初期最容易踩的大坑。症状表现为:Gradle同步失败,或者编译时出现kotlin-stdlib版本冲突、Compose编译器扩展版本与Kotlin不匹配之类的报错。

排查思路说起来很简单,第一步先看根build.gradle.kts中插件版本的组合关系;第二步对照官方版本兼容列表,逐项核对Kotlin、Compose编译器插件、Compose BOM这组三方版本。三个版本只要配顺了,后面99%的版本问题都不存在。

还有一个技巧:在模块build.gradle.kts中使用Compose BOM来管理Compose相关依赖版本。BOM是一份依赖版本清单,声明它之后,各Compose库可以省略版本号,由BOM统一调配。这样你只维护一个版本号,就能控制Compose全家桶的一致性,比逐个库手写版本省心很多,也能减少版本参差的概率。

4.2 预览不显示

写了@Preview注解,右边预览面板却一直空白或报错,这件事挺闹心。常见原因有:

  • 没有引入ui-tooling-preview依赖,预览功能没被激活。
  • 预览函数本身编译有错误,比如调用了不在预览环境支持范围内的API。
  • 主题相关类初始化失败,比如使用了依赖Context资源的自定义主题。

排查方式:先确认预览面板有没有报具体的编译错误,若有就根据错误信息定位;没有的话,看预览函数是不是写在了与主代码相同的模块。还有一类比较隐蔽,是预览函数声明里传入了不可为空的参数,且没有提供默认值。预览时框架无法实例化参数,就会静默失败。给预览函数的参数加默认值就能解决。

4.3 重组次数比预期多

这个现象很难直接观测,但确实会存在。出现问题最常见的原因,是在可组合函数中直接修改状态:

@Composable fun BadExample() { var count by remember { mutableStateOf(0) } count++ // 每次重组都执行,死循环 Text("$count") }

这段代码一旦运行,count在重组时又加1,又触发新的重组,直接耗尽性能。

正确姿势是,不要在可组合函数的直接执行体中修改状态,把修改放到回调事件里,比如前面CounterApp示例中写在Button的onClick里。这样"状态变化导致重组、重组又导致状态变化"的反向循环就不会发生。

4.4 大列表卡顿

第一个项目通常不会遇到真实的大列表问题,但提前知道这个坑没坏处。Compose还没有原生提供针对非数据类列表的自动优化机制,所以如果你自己写了一个for循环来生成长列表,滑动时可能不跟手。

通用的解决方案是用LazyColumn替代手动循环,它是Compose的懒加载列表组件,类似View体系里的RecyclerView。只有当列表项滑入可视区域时才创建对应项,离开可视区域时释放,性能表现好很多。第一个项目不需要真的做大列表,但掌握这个习惯,后面就不会走弯路。

4.5 中文显示问题

模板代码里默认文本是英文,换成中文后偶尔出现显示不太正常的情况。多数情况下是字体问题,或者某些区域显示时因为高度、宽度没留足导致截断。处理方式其实不复杂:在包管理文件里显式引入一款支持中文的字体,在自定义主题里配置字体Family,做一个全局字体方案,中英文显示都会稳定很多。

但如果你只是预览面板里看到中文显示成方框,先检查自己的系统是否缺少对应字体,这是系统环境问题,不是代码问题。遇到这种情况,规避方式是用Preview注解时至少先把关键路径跑通。

5. 继续深入的方向:第一个项目之后怎么走

把第一个Compose项目跑通,只是一个开始。接下来有几条明确的深化路径,按自己的业务方向选就可以了。

第一条线是充实组件库。把Material3提供的高频组件过一遍:按钮的分类与样式、输入框的状态与显示、Switch和Slider等交互控件的使用、Snackbar和Dialog等反馈组件。这些是日常开发的基础弹药库,熟悉它们的参数和写法,后面写任何界面都不慌。

第二条线是加深状态管理能力。单屏幕内用remember和mutableStateOf够用,但一旦多个页面共享同一份数据,或者界面状态需要跟业务数据联动,就需要把状态提升到ViewModel里,跟Jetpack架构组件配合。搞懂"界面状态在Compose层、业务状态在ViewModel层"的分工,是进阶的关键分水岭。后续你可以研究StateFlow与collectAsState的配合方式,用这套组合来替代手动回调。

第三条线是导航与多页面结构。导航组件提供了Compose环境下的路由方案。理解导航图、路由参数传递、返回栈管理,才能把单个页面扩展到完整的App骨架。

第四条线是深挖Compose的渲染原理。如果你对性能优化感兴趣,可以在掌握基础后研究Compose的phase机制,理解重组、布局、绘制在不同阶段是怎么协同工作的。这部分内容稍硬核,但理解后对排查疑难性能问题帮助巨大。

我个人做Compose改造的经验是:只动手写一两个示例项目,跟真正把一个几十个页面的业务模块迁移到Compose相比,深度完全不在一个量级。遇到真实的业务复杂度,比如列表多类型混排、状态频繁更新、页面复用复杂等情况,才会真正逼着你把重组边界、状态提升、惰性加载这些概念用起来。

如果时间有限,优先聚焦第二和第三条线,这两条主线基本覆盖了大部分App开发的核心场景。组件ologi的积累可以边做项目边沉淀,碰到什么组件就深入研究什么组件,反而记得更牢。

第一个项目带来的核心价值,是让你建立"函数即UI、状态即数据"的思维模型。思维模型正确了,后面的路会顺畅得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询