现代 Android UI 开发正逐步从命令式 XML 向声明式 Compose 转变。Compose 凭借其简洁、高效、易测试的特点,能够让开发者更专注于界面和业务逻辑,而不必陷入大量模板化的代码。手把手带你构建一个完整的 Todo List 应用,并演示如何借助自动化工具大幅提升开发效率。
findViewById
的繁琐。@Composable
函数,易于拆分、复用和测试。remember
、mutableStateOf
等 API,让状态驱动界面更新,重组(recomposition)高效且可预测。Android Studio:推荐使用 Arctic Fox (2020.3.1) 或更高版本。
Kotlin:版本 1.8.0+。
Gradle 配置:
// 项目级 build.gradle
buildscript {
ext {
compose_version = '1.5.0'
kotlin_version = '1.8.0'
}
dependencies {
classpath "com.android.tools.build:gradle:8.0.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
// 模块级 build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id "kotlin-kapt"
id "io.github.raamcosta.compose-destinations" version "1.8.5"
}
android {
compileSdk 34
defaultConfig {
applicationId "com.example.todo"
minSdk 21
targetSdk 34
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation "androidx.core:core-ktx:1.10.1"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material3:material3:1.1.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.0"
implementation "androidx.activity:activity-compose:1.7.0"
implementation "io.github.raamcosta.compose-destinations:animations-core:1.8.5"
kapt "io.github.raamcosta.compose-destinations:ksp:1.8.5"
}
Tips: 在 Android Studio 设置里(Preferences → Experimental)确保已开启 Compose 支持。
@Composable
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
View
、Activity
等概念,只有函数调用嵌套。@Composable
fun ProfileCard() {
Row(modifier = Modifier.padding(16.dp)) {
Image(/*…*/)
Column(modifier = Modifier.padding(start = 8.dp)) {
Text("Alice")
Text("Android Developer")
}
}
}
Modifier
).fillMaxWidth().padding(8.dp).background(Color.Gray)
size()
, padding()
, background()
, clickable()
……mutableStateOf
与 remember
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}
}
remember
:组件重组时保留状态mutableStateOf
:包装可观察状态,值变时自动触发重组StateFlow
结合 Compose在 ViewModel 中:
class TodoViewModel : ViewModel() {
private val _todos = MutableStateFlow<List<Todo>>(emptyList())
val todos: StateFlow<List<Todo>> = _todos
fun load() { /*…*/ }
}
在 Composable:
@Composable
fun TodoList(viewModel: TodoViewModel = hiltViewModel()) {
val list by viewModel.todos.collectAsState()
LazyColumn { items(list) { TodoItem(it) } }
}
@Composable
fun MyTheme(content: @Composable ()->Unit) {
MaterialTheme(
colorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC6)
),
typography = Typography(/*…*/),
content = content
)
}
@Composable
fun MyButton(text: String, onClick: ()->Unit) {
Button(
onClick = onClick,
shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth()
) {
Text(text.uppercase(), fontWeight = FontWeight.Bold)
}
}
LaunchedEffect(key)
:基于键启动协程SideEffect
:在每次成功重组后执行DisposableEffect
:绑定 / 解绑资源remember
缓存计算结果derivedStateOf
:避免不必要重组@Composable
fun PulsingDot() {
val scale by animateFloatAsState(
targetValue = if (expanded) 1.5f else 1f,
animationSpec = tween(1000, easing = LinearEasing)
)
Box(modifier = Modifier.size(50.dp).graphicsLayer { scaleX = scale; scaleY = scale }
.background(Color.Red, CircleShape)
.clickable { expanded = !expanded })
}
Live Templates
在 IDE 中预设 Compose 代码片段,如 @cmp
:
@Composable
fun $NAME$() {
$END$
}
极大减少样板代码输入。
Compose Destinations
@Destination
GitHub Copilot / AI 助手
// TODO: fetch from repo
Screenshot Testing(Paparazzi)
com.example.todo
├── MainActivity.kt
├── navigation/ // Destinations 生成
├── ui/
│ ├── TodoListScreen.kt
│ └── EditTodoScreen.kt
└── viewmodel/
└── TodoViewModel.kt
@Destination(start = true)
@Composable
fun TodoListScreen(
navigator: DestinationsNavigator,
viewModel: TodoViewModel = hiltViewModel()
) {
val todos by viewModel.todos.collectAsState()
Scaffold(
topBar = { TopAppBar(title = { Text("我的待办") }) },
floatingActionButton = {
FloatingActionButton(onClick = { navigator.navigate(EditTodoScreenDestination(null)) }) {
Icon(Icons.Default.Add, contentDescription = "添加")
}
}
) { padding ->
LazyColumn(modifier = Modifier.padding(padding)) {
items(todos) { todo ->
Card(modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { navigator.navigate(EditTodoScreenDestination(todo.id)) }
) {
Text(todo.title, modifier = Modifier.padding(16.dp))
}
}
}
}
}
@HiltViewModel
class TodoViewModel @Inject constructor(
private val repo: TodoRepository
): ViewModel() {
private val _todos = MutableStateFlow<List<Todo>>(emptyList())
val todos: StateFlow<List<Todo>> = _todos
init { loadTodos() }
fun loadTodos() = viewModelScope.launch {
_todos.value = repo.getAllTodos()
}
fun save(todo: Todo) = viewModelScope.launch {
repo.save(todo)
loadTodos()
}
}
Compose UI Test
@get:Rule val rule = createComposeRule()
@Test fun addButtonNavigates() {
rule.setContent { TodoListScreen(/*…*/) }
rule.onNodeWithContentDescription("添加").performClick()
rule.onNodeWithText("编辑待办").assertExists()
}
Paparazzi 截图测试
@Test fun todoListSnapshot() = paparazzi.snapshot {
TodoListScreen(/*…*/)
}
GitHub Actions
./gradlew testDebugUnitTest
和 ./gradlew testDebugAndroidTest
derivedStateOf
:复杂计算结果缓存。LaunchedEffect
。