Kotlin/Jetpack Compose 开发技能
概览
这项技能提供了使用 Kotlin 和 Jetpack Compose 进行原生 Android 开发的专家能力。它能够生成 Compose UI 组件,实现现代 Android 架构模式,并全面操作 Gradle 构建。
允许使用的工具
bash- 执行 Gradle 命令,adb 和 Android SDK 工具read- 分析 Kotlin 源文件和 Gradle 配置write- 生成和修改 Kotlin 代码和 Compose composablesedit- 更新现有的 Kotlin 代码和配置glob- 搜索 Kotlin 文件和 Android 资源grep- 在 Android 代码库中搜索模式
能力
Jetpack Compose
-
可组合函数
- 创建 Material Design 3 composables
- 使用 Layout composable 实现自定义布局
- 构建可重用的 UI 组件
- 生成预览注释
- 创建多预览配置
-
状态管理
- 实现 remember 和 rememberSaveable
- 使用状态提升模式
- 配置 derivedStateOf 以计算状态
- 实施 snapshotFlow 以进行副作用处理
- 处理配置更改
-
导航
- 使用 NavHost 配置 Compose 导航
- 实现带参数的类型安全导航
- 设置嵌套导航图
- 使用 NavDeepLink 处理深度链接
- 使用 BottomNavigation 实现底部导航
ViewModel 集成
-
ViewModel 模式
- 使用 Hilt 注入创建 ViewModel
- 实施 StateFlow 和 SharedFlow
- 配置 SavedStateHandle
- 处理进程死亡恢复
- 使用 viewModelScope 进行协程
-
UI 状态管理
- 设计密封类 UI 状态
- 实施加载、错误和成功状态
- 配置下拉刷新
- 使用 Paging 3 处理分页
- 实施搜索和防抖
Kotlin 协程
-
协程模式
- 配置 CoroutineScope 和调度器
- 实施结构化并发
- 正确处理取消
- 使用 SupervisorJob 进行错误隔离
- 配置 ExceptionHandler
-
Flow 模式
- 创建冷和热 Flows
- 实施 StateFlow 和 SharedFlow
- 配置 Flow 操作符
- 使用 buffer 处理背压
- 使用 map、filter、combine 转换 Flows
依赖注入
- Hilt 集成
- 配置 Hilt 模块
- 实施 @Inject 注解
- 设置 ViewModelComponent
- 配置 Singleton 和 Scoped 绑定
- 使用 @EntryPoint 为框架类
Gradle 构建系统
-
构建配置
- 使用 Kotlin DSL 配置 build.gradle.kts
- 设置构建变体和风味
- 配置 ProGuard/R8 规则
- 实施版本目录
- 设置复合构建
-
KSP/KAPT
- 配置 Kotlin 符号处理
- 使用 KSP 设置 Room
- 使用 KAPT 配置 Hilt
- 使用自定义处理器生成代码
测试
- 测试框架
- 编写 JUnit 5 单元测试
- 实施 Compose UI 测试
- 配置测试规则和固定装置
- 使用 MockK 模拟依赖项
- 设置 Robolectric 进行 JVM 测试
目标流程
这项技能与以下流程集成:
jetpack-compose-ui.js- Compose UI 开发android-room-database.js- Room 持久性firebase-cloud-messaging.js- FCM 集成android-playstore-publishing.js- Play Store 提交
依赖项
必需
- Android Studio Hedgehog 或更高版本
- Android SDK 34+
- Kotlin 1.9+
- Gradle 8.2+
可选
- Android 模拟器
- Firebase 工具
- adb (Android Debug Bridge)
- Layout Inspector
配置
项目结构
app/
├── src/
│ ├── main/
│ │ ├── kotlin/com/example/myapp/
│ │ │ ├── MyApplication.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── di/
│ │ │ │ └── AppModule.kt
│ │ │ ├── data/
│ │ │ │ ├── repository/
│ │ │ │ ├── local/
│ │ │ │ └── remote/
│ │ │ ├── domain/
│ │ │ │ ├── model/
│ │ │ │ ├── repository/
│ │ │ │ └── usecase/
│ │ │ └── ui/
│ │ │ ├── theme/
│ │ │ ├── navigation/
│ │ │ └── feature/
│ │ ├── res/
│ │ └── AndroidManifest.xml
│ ├── test/
│ └── androidTest/
├── build.gradle.kts
└── proguard-rules.pro
版本目录
# gradle/libs.versions.toml
[versions]
kotlin = "1.9.21"
compose-bom = "2024.01.00"
hilt = "2.50"
room = "2.6.1"
lifecycle = "2.7.0"
[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
[plugins]
android-application = { id = "com.android.application", version = "8.2.0" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version = "1.9.21-1.0.16" }
使用示例
创建可组合屏幕
// ui/feature/home/HomeScreen.kt
package com.example.myapp.ui.feature.home
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
onItemClick: (String) -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Home") }
)
}
) { paddingValues ->
when (val state = uiState) {
is HomeUiState.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is HomeUiState.Success -> {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(state.items, key = { it.id }) { item ->
ItemCard(
item = item,
onClick = { onItemClick(item.id) }
)
}
}
}
is HomeUiState.Error -> {
ErrorContent(
message = state.message,
onRetry = viewModel::retry,
modifier = Modifier.padding(paddingValues)
)
}
}
}
}
@Composable
private fun ItemCard(
item: Item,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
onClick = onClick,
modifier = modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = item.title,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = item.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
实施 ViewModel
// ui/feature/home/HomeViewModel.kt
package com.example.myapp.ui.feature.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val getItemsUseCase: GetItemsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading)
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
init {
loadItems()
}
private fun loadItems() {
viewModelScope.launch {
_uiState.value = HomeUiState.Loading
getItemsUseCase()
.catch { e ->
_uiState.value = HomeUiState.Error(e.message ?: "Unknown error")
}
.collect { items ->
_uiState.value = HomeUiState.Success(items)
}
}
}
fun retry() {
loadItems()
}
}
sealed interface HomeUiState {
data object Loading : HomeUiState
data class Success(val items: List<Item>) : HomeUiState
data class Error(val message: String) : HomeUiState
}
配置导航
// ui/navigation/NavGraph.kt
package com.example.myapp.ui.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.example.myapp.ui.feature.home.HomeScreen
import com.example.myapp.ui.feature.detail.DetailScreen
sealed class Screen(val route: String) {
data object Home : Screen("home")
data object Detail : Screen("detail/{itemId}") {
fun createRoute(itemId: String) = "detail/$itemId"
}
}
@Composable
fun NavGraph(
navController: NavHostController,
startDestination: String = Screen.Home.route
) {
NavHost(
navController = navController,
startDestination = startDestination
) {
composable(Screen.Home.route) {
HomeScreen(
onItemClick = { itemId ->
navController.navigate(Screen.Detail.createRoute(itemId))
}
)
}
composable(
route = Screen.Detail.route,
arguments = listOf(
navArgument("itemId") { type = NavType.StringType }
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId") ?: return@composable
DetailScreen(
itemId = itemId,
onBackClick = { navController.popBackStack() }
)
}
}
}
配置 Hilt 模块
// di/AppModule.kt
package com.example.myapp.di
import android.content.Context
import androidx.room.Room
import com.example.myapp.data.local.AppDatabase
import com.example.myapp.data.remote.ApiService
import com.example.myapp.data.repository.ItemRepositoryImpl
import com.example.myapp.domain.repository.ItemRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
}
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
@Provides
fun provideItemDao(database: AppDatabase) = database.itemDao()
}
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
构建命令
# 清理构建
./gradlew clean
# 构建 debug APK
./gradlew assembleDebug
# 构建 release AAB
./gradlew bundleRelease
# 运行单元测试
./gradlew testDebugUnitTest
# 运行仪器测试
./gradlew connectedDebugAndroidTest
# 运行 lint
./gradlew lintDebug
# 安装在设备上
./gradlew installDebug
质量门
代码质量
- Kotlin 编译器警告视为错误
- Detekt/ktlint 合规性
- 无弃用 API 使用
- 适当的协程作用域管理
性能基准测试
- 应用启动 < 500ms(冷启动)
- 滚动和动画期间 60fps
- 无内存泄漏(LeakCanary 验证)
- 最小化重组
测试覆盖率
- 单元测试覆盖率 > 80%
- 关键流程的 UI 测试覆盖率
- UI 组件的截图测试
错误处理
常见问题
-
Gradle 同步失败
./gradlew --refresh-dependencies -
KSP/KAPT 问题
./gradlew clean && ./gradlew kspDebugKotlin -
模拟器问题
adb kill-server && adb start-server -
Compose 预览问题
# 使缓存无效:文件 > 使缓存无效 / 重启
相关技能
android-room- Room 数据库集成firebase-mobile- Firebase 服务mobile-testing- 全面测试google-play-console- Play Store 发布
版本历史
- 1.0.0 - 初始发布,具有核心 Kotlin/Compose 功能