☰
从零创建第一个Compose项目:声明式UI与状态驱动开发实战
2026/10/10 6:55:51 网站建设 项目流程

1. 第一个Compose项目:整体思路与选型

1.1 为什么从Compose开始

先说结论:如果你正准备接触Android界面开发,Compose这条路现在走非常合适。传统View体系的XML布局写了这么多年,学习曲线其实不低。你切到布局文件,写完一个控件,再切回Activity里面去找id,绑定事件,来回跳转。一套界面写下来,时间都耗在“切换上下文”上了。Compose的思路完全不同——界面直接用Kotlin代码描述,布局和逻辑写在同一处,所见即所得,代码即UI。

我做过一段时间传统View项目,刚开始切到Compose也有点不习惯。但用了一周之后,发现回不去了。并不是说Compose就一定比传统View更“高级”,而是它把Android界面开发真正拉回到了“一门语言、一套逻辑”的轨道上:状态驱动UI,数据变了界面自动更新,不用再手动写findViewById、setOnClickListener这一堆样板代码。声明式UI这个名词看起来玄乎,其实说白了就是:你告诉界面“现在该长成什么样”,剩下的事情框架替你操心。

这篇博文从零开始,带你完整走一遍“创建第一个Compose项目”的过程。内容包括:环境准备、项目创建、结构拆解、核心概念、实操页面、问题排查、扩展方向。不管你是刚入行的新人,还是写了几年XML布局想转Compose的老手,按这篇的顺序过一遍,一个能跑、能交互、能改着玩的Compose项目就出来了。

1.2 选型背后的三个关键考量

创建第一个Compose项目之前,有三件比较关键的事值得先想清楚,能省掉后面很多折腾。

第一,IDE选哪个版本。Compose和Android Studio的绑定比较紧密。新版本Studio对Compose的支持通常更好,比如布局预览、实时刷新、Compose调试工具这些。我用的是某个较新的稳定版Studio,开发体验和几年前相比差别很大。建议选主流稳定版,别追预览版,预览版偶尔会有插件不兼容的坑。

第二,Kotlin版本和Compose编译器插件的关系。Compose编译器插件不一定和Kotlin版本绑死。在Kotlin 2.0之前,需要单独配置composeOptions里面的kotlinCompilerExtensionVersion来匹配Kotlin版本。比如你用的Kotlin是1.9.x,那Compose编译器可能得配对应的版本号。Kotlin 2.0之后,Compose编译器插件直接随Kotlin走,配置方式简化了很多,但也意味着升级Kotlin大版本时要留意兼容性。这块是新手最容易卡住的地方之一,后面创建项目时会具体说。

第三,AGP版本和Gradle版本的对应关系。Android Gradle插件和Gradle本身有个兼容表,版本不匹配会直接报错。比如AGP 8.x就需要Gradle 8.x及以上。好消息是,通过Android Studio的模板创建项目,IDE会自动生成一套互相兼容的版本配置,新手不需要自己手动去查兼容表。自己手工配的时候才容易踩坑。

1.3 项目模板:别选错起点

Android Studio新建项目的时候会弹出一堆模板:Empty Activity、Basic Activity、Bottom Navigation Activity等等。第一个项目,就选Empty Activity,别整那些自带导航栏、自带菜单的模板。

原因有两个。第一,模板自带的东西越多,你要先读懂的无关注释和样板代码就越多,容易迷失在“哪些代码是我写的”里面。第二,Empty Activity其实是“空壳”模板,它只是帮你生成一个MainActivity和一个Compose入口,剩余空间全是你的。我见过不少初学者一上来选了带导航的模板,结果光删模板自带的导航组件就花了一个晚上。第一步走简单点,心里踏实。

创建完之后,项目里默认会有一个 MainActivity.kt,点开就能看到下面的核心代码结构。这个结构就是你认识Compose的第一个窗口。

2. 创建项目的完整实操流程

2.1 从新建项目到第一个界面

这里把整个流程按我用的时候的实际操作顺序写一遍。不同版本的Studio界面细节可能略有差异,但大的步骤是通用的。

打开Android Studio,先确认没有开着什么别的项目,或者直接New Project。在弹出的窗口左侧选“Empty Activity”,然后填写项目信息:

  • Name:项目名称。第一个项目可以起一个有意义的名字,比如MyFirstCompose。注意项目名会直接用作包名的一部分,尽量不要带中文和空格。
  • Package name:默认会根据公司域名加项目名生成。自己做项目的话,格式一般长这样:com.example.myfirstcompose。这里自己起,注意包名规则:全部小写,用点分隔,每一段不能以数字开头。
  • Save location:项目存储路径。建议别放在系统盘很深的位置,找一个好找的目录,比如D:\AndroidProject下面。
  • Language:选Kotlin。Compose本身完全构建在Kotlin之上,选Kotlin是必须的。
  • Minimum SDK:选一个合适的API Level。如果只是自己做着玩,建议直接选API 24或API 26。API 24对应Android 7.0,覆盖了绝大部分存量设备。选太低的API会限制你能用的Compose API,选太高则很多老手机装不上。

填写完之后点Finish,Gradle会开始同步。第一次同步可能要下载不少依赖,时间长短取决于网络环境。等它结束,右下角进度条消失,左侧项目树也正常显示了,项目就算创建成功。

2.2 关键配置项解析

项目建好后,先别急着写代码,打开app/build.gradle.kts,看几个关键配置。用模板创建的项目,Compose相关配置基本都给你配好了,不用动。但你要知道它们是什么意思,后面出了问题才知道去哪找。

android { namespace = "com.example.myfirstcompose" compileSdk = 34 defaultConfig { applicationId = "com.example.myfirstcompose" minSdk = 24 targetSdk = 34 versionCode = 1 versionName = "1.0" } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } buildFeatures { compose = true } }

重点看两块。

第一,buildFeatures { compose = true }。这行是开启Compose功能的开关。模板默认给你开好,但如果你以后自己创建一个不带Compose的项目,想手动接Compose,第一件事就是加这行。没加这个配置,你写的Compose相关代码全都编不过。

第二,Compose依赖的引入方式。模板默认用的是dependencies块里面一堆implementation(platform("androidx.compose:compose-bom:xxx"))之类的写法。BOM(Bill of Materials)是Compose的依赖版本管理方案,你不需要一个一个指定Compose库的版本号,只需要指定BOM的版本,它内部的库都是经过兼容性测试的。这种方式强烈推荐,因为它解决了一个经典问题:各个Compose库版本不匹配导致的诡异编译报错。

如果你用的Kotlin 2.0及以上版本,模板里一般还会带一个org.jetbrains.kotlin.plugin.compose插件。这是新版Compose编译器插件的配置位置。在老版本里,你需要在composeOptions里手动配kotlinCompilerExtensionVersion。新方案简单很多,基本不用管版本号匹配的事了。

2.3 首次构建的注意事项

首次构建有两个比较常见的坑,建议提前有心理准备。

第一个是依赖下载慢。Compose相关依赖和Kotlin编译器插件体积都不小,首次同步可能要下载几百MB的包。如果网络环境不太理想,同步会卡很长时间。一个靠谱的做法是:在项目根目录的settings.gradle.kts里把仓库地址换成国内镜像。具体写法在项目模板里已经配好了google()、mavenCentral(),你可以在它们前面加一个镜像仓库地址。注意顺序很重要,镜像放前面,能命中镜像的依赖会先走镜像。

第二个是JDK版本问题。新版Android Studio要求JDK 17及以上。如果你本机装的是JDK 8或11,Gradle同步会直接报错。这里有个便捷手段:Android Studio自带一个JBR(JetBrains Runtime),它就是一套完整的JDK。在File -> Project Structure -> SDK Location里可以设置Gradle JDK路径,直接选Android Studio内置的版本,省去你自己配环境变量的麻烦。我做项目时一直都是这么配的,基本没被JDK版本卡过。

3. 初识Compose项目结构

3.1 MainActivity与setContent

创建完项目后,打开MainActivity.kt,完整代码如下:

package com.example.myfirstcompose import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import com.example.myfirstcompose.ui.theme.MyFirstComposeTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyFirstComposeTheme { Greeting("Android") } } } } @Composable fun Greeting(name: String, modifier: Modifier = Modifier) { Text( text = "Hello $name!", modifier = modifier ) } @Preview(showBackground = true) @Composable fun GreetingPreview() { MyFirstComposeTheme { Greeting("Android") } }

这段代码看着不大,里面包含了Compose的全部核心骨架。拆开来看。

setContent是连接Activity和Compose的入口方法。它做的事情简单说就是:在Activity允许的范围内,创建一块专门承载Compose界面的内容区域。你写的Compose界面就放在这个花括号里面。传统View体系下你用setContentView(R.layout.activity_main)加载布局文件,对应到Compose就是用setContent放进代码写的界面。

@Composable注解是整个Compose体系的标记。函数加上这个注解,就表示它是个“可组合函数”,可以被Compose编译器识别并做特殊处理。注意这个处理并不是简单的函数调用,Compose编译器会把可组合函数转换成可以跟踪状态、按需更新的结构。所以可组合函数不能随便脱离Compose环境调用,比如你不能在一个普通函数里直接调用Greeting("Android"),编译器会直接报错——可组合函数只能在其他可组合函数里调用。

Greeting函数的定义里有个细节:参数modifier: Modifier = Modifier。这是一个约定俗成的写法。Compose里面几乎所有自定义可组合函数,都把modifier作为第一个或最后一个参数,默认值是空Modifier。这样外部调用方可以自由传各种修饰符进来调整大小、间距、对齐方式,而函数内部不用提前写死。如果自定义组件不带这个参数,以后想在外面调整它的时候就会很别扭,得回函数内部改代码。

3.2 核心依赖与版本对齐

项目里还有一个文件是ui/theme包下的Theme.kt、Color.kt、Type.kt。这三个文件是模板生成的颜色、字体、主题定义。先说它们的作用,再说不急着改它们的理由。

在传统View体系里,换主题经常要改一堆XML资源,还要注意不同Android版本上的兼容表现。Compose的MaterialTheme方案集成在代码里。你定义好颜色方案、字体方案、形状方案,然后通过MaterialTheme组合函数把它提供给整个界面树。

比如模板里MyFirstComposeTheme大致长这样:

@Composable fun MyFirstComposeTheme( darkTheme: Boolean = isSystemInDarkTheme(), dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { dynamicColor -> if (darkTheme) DynamicDarkColorScheme.getInstance(context) else DynamicLightColorScheme.getInstance(context) darkTheme -> DarkColorScheme else -> LightColorScheme } MaterialTheme( colorScheme = colorScheme, typography = Typography, content = content ) }

这里的isSystemInDarkTheme()很有意思。它是个可组合函数,用来观察系统是否处于深色模式。如果用户在系统设置里切换了深色模式,这个函数会自动获取到最新状态,然后MyFirstComposeTheme会用新的颜色方案重绘整个界面。这就是Compose的响应式状态应用在主题上的一个最直观例子。传统View想要动态跟随系统深色模式,要写一堆配置和监听逻辑,Compose里直接内置了。

不过第一个项目阶段,你其实不需要动这几个主题文件。直接用模板默认提供的就行。我建议先跑起来,对Compose的基础概念有感觉了再回来改颜色、字体。上来就改主题,容易一头扎进Material Design的细节里出不来。

3.3 资源文件与Compose的边界

项目里还有一个res目录,里面有drawable、mipmap、values等子目录。values下面有strings.xml、themes.xml。

Compose虽然用代码声明UI,但并没有取消Android资源系统。应用图标、字符串、启动主题这些仍然走资源目录。比如AndroidManifest.xml里android:label指向的字符串来自strings.xml,启动时显示的启动页背景来自themes.xml定义的样式。

所以Compose项目的实际形态是混合的:资源和传统View项目一样管理,界面的主体部分用Compose代码描述。你仍然需要理解Android资源系统的基本用法,但不能所有事情都用Compose那套思维硬套。这是很多新人理解偏的地方,以为Compose项目就是纯Kotlin,连图片资源都要用代码画出来。实际上,把图片放进res/drawable或res/mipmap,再通过Image组件加载,是效率更高也更常见的做法。

4. 核心概念详解:看穿Compose的底层逻辑

4.1 可组合函数:UI是函数调用的结果

Compose界面里最常见的代码是这样的:

@Composable fun UserCard(userName: String) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text(text = userName, style = MaterialTheme.typography.titleLarge) Spacer(modifier = Modifier.height(8.dp)) Text(text = "点击卡片查看详情", style = MaterialTheme.typography.bodyMedium) } }

这段代码定义了一个卡片组件。它接收一个userName参数,然后通过Column布局把两行文字上下排列。理解这段代码的关键不在函数本身,而在于它执行的时候发生了什么。

你写的@Composable函数在运行时被Compose框架按照特殊方式执行。框架会记录这次执行用到了哪些数据(比如userName)、生成了哪些UI节点。当userName发生变化时,框架能够精确地知道“只有这个Text的文字需要更新”,然后只重新执行相关的那部分代码生成新的界面,其他部分保持不变。这个机制叫做重组。

这个机制带来的行为特点是:界面永远和数据保持一致。你不需要主动“更新”UI,只需要修改数据来源,UI会自己跟上。刚开始可能觉得这个思维模式有点绕,我建议你做一个心理转换:不要再想“我要让这个控件显示什么”,而是想“这个控件应该反映什么数据”。

可组合函数还有一个特点是执行顺序不一定。Compose框架有自己的一套调度机制,在某些情况下可能会重新排列组合函数的执行顺序。所以不要在可组合函数里写有副作用的代码,比如修改一个不属于Compose状态的外部变量、读写文件、发起网络请求。这些操作应该放到协程、回调或者LaunchedEffect等特殊机制里去做。刚开始把握不准也没关系,先记住这个原则,不会出大错。

4.2 状态与重组:界面自动更新的核心秘密

状态是Compose里最重要的名词。如果不理解它,写出来的代码经常会出现“界面怎么不刷新”的困惑。

看个例子。我们想做一个点按钮、计数的界面:

@Composable fun CounterPage() { var count by remember { mutableStateOf(0) } Column( modifier = Modifier .fillMaxSize() .padding(24.dp) ) { Text(text = "当前计数:$count", fontSize = 20.sp) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { count++ }) { Text(text = "点我加一") } } }

拆解一下这里发生了什么。

remember { mutableStateOf(0) }创建了一个初始值为0的状态对象。remember的作用是让这个对象在重组过程中被保留。如果不加remember,每次重组都会重新执行mutableStateOf(0),count会被重置回0,界面永远不动。

by关键字是Kotlin的委托语法。var count by ...实际上等价于val state = remember { mutableStateOf(0) }; var count = state.value。平时写的时候直接用by简洁一些,不过要记得文件头部引入import androidx.compose.runtime.getValue和import androidx.compose.runtime.setValue这两个扩展函数。IDE一般会自动补全,但如果手动抄代码漏了import,会报一个“unresolved reference: getValue”的错误。

Button(onClick = { count++ })里做的事情是:修改count的值。而count是个状态,Compose在后台能看到这个状态被修改了。于是框架会在合适的时机重新执行CounterPage这个函数,拿到最新的count值,重新生成包含新文字的Text。这就完成了“点一下,界面数字加一”的效果。

这个过程还有一个最小重组的特性值得知道:Compose并不是把整个界面树全部重建一遍,它会精确比较哪些部分依赖的状态变了,只更新那些部分。上面这个例子,重组时只会重新执行读取了count的地方。如果界面上还有其他不依赖count的子组件,它们不会被重新执行。

初学阶段有一个常见问题:觉得到处都要写remember很麻烦。其实不必处处加。只有那些“界面需要反映它的变化”的数据,才需要变成状态并交给Compose观察。一次性用到的常量字符串,直接写在代码里就好,不需要remember。

4.3 Modifier修饰符:布局外观的统一入口

Compose里面想调整组件的大小、间距、对齐、背景、边框、点击事件,全部通过Modifier串联。这种设计可能一开始不习惯,但用顺手之后会发现非常高效。

Text( text = "Hello Compose", modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) .background(Color.LightGray) .clickable { /* 点击逻辑 */ } )

Modifier后面的每一行都是一个修饰项,从上到下依次生效。比如padding(horizontal = 16.dp)在background之前,那么背景只画在padding之后的区域内;如果顺序反过来,padding也会被画上背景色。Modifier的顺序会影响最终显示效果,这是个初学者很容易忽略的点。

常用的几个Modifier那里,fillMaxWidth()和fillMaxSize()控制组件占满父容器的宽度或大小;padding()控制内边距,可以用padding(16.dp)统一设置,也可以padding(top = 8.dp, bottom = 8.dp)分开设置;size()直接指定宽高;clip()做形状裁剪,比如clip(RoundedCornerShape(8.dp))让组件背景变成圆角;clickable让任意组件可点击。

初步上手时,建议先只掌握fillMaxWidth、padding、size、clickable这几个,足够做很多布局了。其他的用到再查,不用一上来全背。

5. 动手实现第一个可交互界面

5.1 从静态界面开始

理论说了不少,现在动手写一个完整的界面。这个界面尽管简单,但涵盖了布局、列表、状态、点击这些核心要素。

假设我们要做一个“待办事项”页面:上面一个输入框,下面一个按钮,点击按钮把输入的文字加入下方列表。

先写静态骨架,从布局开始:

@Composable fun TodoPage() { var inputText by remember { mutableStateOf("") } val todoList = remember { mutableStateListOf<String>() } Column( modifier = Modifier .fillMaxSize() .padding(16.dp) ) { Text( text = "我的待办", style = MaterialTheme.typography.headlineMedium ) Spacer(modifier = Modifier.height(16.dp)) TextField( value = inputText, onValueChange = { inputText = it }, placeholder = { Text(text = "输入待办事项") }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) Button( onClick = { /* 先空着 */ }, modifier = Modifier.fillMaxWidth() ) { Text(text = "添加") } Spacer(modifier = Modifier.height(16.dp)) Text( text = if (todoList.isEmpty()) "还没有待办事项" else "共 ${todoList.size} 项", style = MaterialTheme.typography.bodyMedium ) } }

这里用了mutableStateListOf(),它是Compose提供的一个可观察的列表。和普通MutableList的区别在于,对列表的增删改操作能被Compose感知到,列表变化时会自动触发界面重组。这个细节非常关键——如果用普通的ArrayList,即使你把数据加进去了,Compose也不知道列表变了,界面不会更新。

运行一下,这个界面已经能显示、能输入,但点按钮还没有实际效果。下一步,把添加逻辑接上。

5.2 添加交互逻辑与列表展示

把Button的点击事件和列表展示补全:

@Composable fun TodoPage() { var inputText by remember { mutableStateOf("") } val todoList = remember { mutableStateListOf<String>() } Column(...) { Text(...) Spacer(...) TextField(...) Spacer(...) Button( onClick = { if (inputText.isNotBlank()) { todoList.add(inputText.trim()) inputText = "" } }, modifier = Modifier.fillMaxWidth() ) { Text(text = "添加") } Spacer(modifier = Modifier.height(16.dp)) Text(...) LazyColumn( modifier = Modifier.fillMaxWidth() ) { items(todoList) { item -> Text( text = item, modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp) ) } } } }

这里有几个点要解释。

if (inputText.isNotBlank())判断输入的文本不是空白才添加,添加之后立刻把inputText清空,这样输入框会随之清空。因为inputText是个状态,TextField的值绑定到它上,所以清空inputText,输入框的文字就被清掉了。这就是单向数据流的思想:输入框的内容在变化时写入状态,状态变化时输入框跟着刷新。界面上的所有变化都来源于状态的变化。

LazyColumn是列表组件的核心。它相当于传统RecyclerView,只渲染屏幕可见范围内的项,数据量很大时性能依然稳定。直接写Column加repeat也能显示列表,但数据量一大就会卡顿,所以列表场景用LazyColumn是标准做法。

items(todoList)是LazyColumn作用域内的一个扩展函数,用于遍历列表生成每一项。每一项就是一个普通的Composable,这里直接放了一个Text。如果你自己写了items找不到索引,需要引入import androidx.compose.foundation.lazy.items。漏掉这个import是常见报错点。

这样运行之后,你就能输入内容、点击按钮、看到列表新增一行,一个最简单的可交互应用完成了。这步跑通之后,Compose的基础链路——状态、重组、事件、列表——就整个走了一遍。

5.3 预览与调试技巧

Compose项目有个很大的优势就是支持实时预览。模板里的@Preview注解配合Android Studio的Preview面板,可以不用运行App就直接看到界面效果。

预览代码如下:

@Preview(showBackground = true, showSystemUi = true) @Composable fun TodoPagePreview() { MyFirstComposeTheme { TodoPage() } }

点编辑器右上角的Preview面板,就能看到界面预览。如果你改了代码,预览会在你写完代码后自动刷新。注意:只有标注了@Preview的可组合函数才会出现在预览面板里,并且预览函数不能接收参数。所以如果你想预览一个有参数的组件,常见做法是包一层无参数的预览函数,在函数体内传入固定的测试数据。

调试方面,Compose在Android Studio里的支持做得不错。可以在Compose预览器里点“切换交互模式”,直接点击预览里的按钮、输入文字,看到真实的交互效果。在真机上调试时,Android Studio带有“Layout Inspector”功能,能够看到Compose的组件树、每个组件的Modifier链、以及当前的组合信息。这个工具对排查“某个组件为什么显示在这个位置”之类的问题非常有用。

还有一个小技巧:如果发现界面刷新和你预期不一样,先在Text里临时把当前状态的打印出来,比如改成Text(text = "count = $count"),通过界面上显示的值来确认状态是否正确更新。Compose Debug模式下也可以打Log,不过每次重组的时候打印的日志会很多,建议只在确实需要验证“有没有重组”时用。

6. 常见问题与排查技巧实录

6.1 编译报错类问题

第一个Compose项目遇到的编译报错,大多数集中在下面几个问题上。

问题一:找不到Composable函数。提示信息类似@Composable invocations can only happen from the context of a @Composable function。原因基本就是你在普通函数里调用了可组合函数。检查一下调用链上有没有漏掉@Composable注解。

问题二:getValue/setValue 未解析。原因通常是漏了import androidx.compose.runtime.getValue和setValue。用by remember { mutableStateOf(...) }语法时必须引入。在Android Studio里按Alt+Enter可以快速引入。

问题三:编译器插件相关报错。这类报错通常和Kotlin或Compose编译器版本有关。如果是Kotlin 2.0以下版本,去app/build.gradle.kts里检查composeOptions中kotlinCompilerExtensionVersion是不是和Kotlin版本匹配。如果是Kotlin 2.0及以上,确认在根目录build.gradle.kts的plugins块里有没有声明org.jetbrains.kotlin.plugin.compose。

问题四:BOM版本不对。如果你自己改过compose-bom版本,注意BOM版本要跟你的Android SDK版本兼容。BOM版本过新,可能需要更高的compileSdk。直接把模板默认的版本保留下来是最稳妥的。

6.2 预览不生效或不刷新

预览面板偶尔会遇到“加载不出来”或“改了代码不更新”的情况。我遇到过几次,解决办法比较固定:

先看@Preview注解有没有加上,预览函数是不是无参的。然后确认你预览的组件没有依赖某些只会在运行时才有的环境,比如网络请求、Context获取这些。如果依赖了,预览会直接显示错误信息,那就需要给预览函数传固定的假数据,或者用PreviewParameter机制提供多组预览数据。

如果代码改了但预览没反应,试一下手动点预览面板右上角的刷新按钮。还不行的话,就点菜单栏的Build -> Clean Project,再等一下重新构建。预览服务偶尔会抽风,重建一次基本上都会恢复。

6.3 版本兼容问题

我们在第1节就说过版本兼容的风险。这里把我在实际项目里遇到的版本问题整理成一张速查表,方便你对照排查。

现象可能原因解决措施
Gradle同步失败,报Gradle版本过低AGP和Gradle版本不匹配确认AGP版本对应的最低Gradle版本,在gradle-wrapper.properties里升级
编译报“Compose compiler”相关错误Kotlin版本和Compose编译器插件版本不匹配Kotlin 2.0以下版本检查composeOptions;2.0以上检查plugins块
使用了某个Compose API但报未定义compileSdk太低或BOM版本太旧提升compileSdk版本,或升级BOM版本
实时刷新失效增量构建缓存异常Clean Project后重新构建

版本兼容问题有个通用的“偷懒”办法:不要手动管理版本号,始终依赖Android Studio模板生成的默认版本。模板生成的版本组合是Google官方测试过的。当你需要升级某个库时,优先升级BOM版本而不是单独升级某个子库,这样整个依赖集合仍然是一致的。

我实际项目里升级过Compose BOM,从旧版本升到新版本,编译之后遇到一个VectorPath相关的兼容问题,最后是把BOM和Kotlin插件版本一起升级才解决。从那以后我养成了一个习惯:升级前端看变更日志,后端看依赖树,双击Shift打开gradle面板里的app -> Tasks -> help -> dependencies,能看到完整的依赖图和版本冲突情况。

7. 项目扩展方向与个人实践体会

7.1 下一步可以做什么

第一个项目跑通之后,下一步随便挑一个方向扩展,都能把Compose的能力再往深推一层。

一个性价比很高的扩展方向是做一个真正的“记事本”App。在现有待办列表的基础上,加入Room数据库持久化存储,让数据不因App重启而消失;加入多页面导航,点击某条待办进入详情页;加入编辑和删除操作,列表项左滑删除;加入主题切换,支持浅色深色模式。这一套走下来,你对Compose的掌握会从“会写页面”提升到“能做完整应用”的层次。

另一个值得折腾的路线是用Material 3组件重构现有界面。模板默认用的就是Material 3,你可以在里面尝试NavigationBar、FloatingActionButton、TopAppBar、Card这些组件,熟悉它们的使用方式和自定义方法。Material 3的设计风格和Material 2有明显差异,用好了界面质感会提升不少。

还有一个比较推荐做的尝试是把Compose和传统View混合使用。在同一个项目里,在Compose的界面中嵌入一个传统的AndroidView,或者反过来在传统布局里嵌入ComposeView。现在很多存量项目并不是纯Compose或者纯View,理解两者之间的桥接方式非常实用。

7.2 我踩过的坑和积累的习惯

最后分享几个个人经验。

第一个坑是过度使用状态。刚开始写Compose时,很容易把每个变量都包成mutableStateOf,结果就是任何一点点界面变动都会触发大面积重组,出现莫名其妙的性能问题。后来我学会了一个判断标准:这个值会不会因为用户操作或外部事件而改变?如果不会,它就不需要是状态。静态配置的数据直接写常量,只有跨时间的动态数据才用状态管理。

第二个坑是低估了Modifier顺序的影响。我之前遇到过一个问题:一个圆角卡片,背景色怎么都盖不满整个卡片区域,左上角有个直角露出来。排查了半天才反应过来,是clip写在background后面了。正确顺序是clip先执行,background沿着裁剪过的形状绘制,这样背景就带上了圆角。从那以后我都盯着一句话:先裁剪,再上色。

第三个心得是关于学习路径的。Compose的API体系很庞大,组件的数量比传统View多很多。如果一开始就想把所有组件都搞明白,很容易产生挫败感。我的建议是:先掌握状态、重组、Modifier、布局组件这四样,其他的用到哪个学哪个。这四样是Compose的地基,地基不牢,学再多的组件也容易飘。

第四个习惯是多写独立的小Demo。每学到一个新组件或者新API,就建一个单独的页面,在一个“组件实验室”App里面集中展示。这样以后要查“某种边框效果怎么写”的时候,翻自己的Demo比翻官方文档还快。我自己就是靠这种方式,大概两周时间内把Compose的列表、网络、主题、导航这些知识点过了一遍。

第一个Compose项目做到这里,可以说已经正式入门了。剩下的东西,无非是在这个基础上不断地写、不断地踩坑、不断地把问题变成经验。祝你在Compose这条路上玩得开心。

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

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

立即咨询