android-native-dev by minimax-ai/skills
npx skills add https://github.com/minimax-ai/skills --skill android-native-dev开始开发前,先评估当前项目状态:
| 场景 | 特征 | 处理方式 |
|---|---|---|
| 空目录 | 没有任何文件 | 需要完整初始化,包括 Gradle Wrapper |
| 已有 Gradle Wrapper | 存在 gradlew 和 gradle/wrapper/ | 直接使用 ./gradlew 进行构建 |
| Android Studio 项目 | 完整的项目结构,可能缺少 wrapper | 检查 wrapper,必要时运行 gradle wrapper |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 存在部分文件 |
| 检查缺失文件,完成配置 |
关键原则 :
./gradlew assembleDebug 能够成功执行gradle.properties,先创建它并配置 AndroidXMyApp/
├── gradle.properties # 配置 AndroidX 及其他设置
├── settings.gradle.kts
├── build.gradle.kts # 根层级
├── gradle/wrapper/
│ └── gradle-wrapper.properties
├── app/
│ ├── build.gradle.kts # 模块层级
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/com/example/myapp/
│ │ └── MainActivity.kt
│ └── res/
│ ├── values/
│ │ ├── strings.xml
│ │ ├── colors.xml
│ │ └── themes.xml
│ └── mipmap-*/ # 应用图标
# 必需配置
android.useAndroidX=true
android.enableJetifier=true
# 构建优化
org.gradle.parallel=true
kotlin.code.style=official
# JVM 内存设置(根据项目大小调整)
# 小型项目: 2048m, 中型: 4096m, 大型: 8192m+
# org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
注意 : 如果在构建时遇到
OutOfMemoryError,请增加-Xmx的值。依赖项较多的大型项目可能需要 8GB 或更多。
dependencies {
// 使用 BOM 管理 Compose 版本
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
// Activity 和 ViewModel
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
}
产品风味允许你创建应用的不同版本(例如,免费/付费,开发/预发布/生产)。
在 app/build.gradle.kts 中的配置 :
android {
// 定义风味维度
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
// 每个风味不同的配置值
buildConfigField("String", "API_BASE_URL", "\"https://dev-api.example.com\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "true")
// 不同的资源
resValue("string", "app_name", "MyApp Dev")
}
create("staging") {
dimension = "environment"
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
buildConfigField("String", "API_BASE_URL", "\"https://staging-api.example.com\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "true")
resValue("string", "app_name", "MyApp Staging")
}
create("prod") {
dimension = "environment"
// 生产环境无后缀
buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "false")
resValue("string", "app_name", "MyApp")
}
}
buildTypes {
debug {
isDebuggable = true
isMinifyEnabled = false
}
release {
isDebuggable = false
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
构建变体命名 : {flavor}{BuildType} → 例如,devDebug, prodRelease
Gradle 构建命令 :
# 列出所有可用的构建变体
./gradlew tasks --group="build"
# 构建特定变体(风味 + 构建类型)
./gradlew assembleDevDebug # Dev 风味,Debug 构建
./gradlew assembleStagingDebug # Staging 风味,Debug 构建
./gradlew assembleProdRelease # Prod 风味,Release 构建
# 构建特定风味的所有变体
./gradlew assembleDev # 所有 Dev 变体(debug + release)
./gradlew assembleProd # 所有 Prod 变体
# 构建特定构建类型的所有变体
./gradlew assembleDebug # 所有风味,Debug 构建
./gradlew assembleRelease # 所有风味,Release 构建
# 将特定变体安装到设备
./gradlew installDevDebug
./gradlew installProdRelease
# 一键构建并安装
./gradlew installDevDebug && adb shell am start -n com.example.myapp.dev/.MainActivity
在代码中访问 BuildConfig :
注意 : 从 AGP 8.0 开始,默认不再生成
BuildConfig。你必须在build.gradle.kts中显式启用它:android { buildFeatures { buildConfig = true } }
// 在代码中使用构建配置值
val apiUrl = BuildConfig.API_BASE_URL
val isLoggingEnabled = BuildConfig.ENABLE_LOGGING
if (BuildConfig.DEBUG) {
// 仅调试代码
}
风味特定源集 :
app/src/
├── main/ # 所有风味共享的代码
├── dev/ # 仅 Dev 的代码和资源
│ ├── java/
│ └── res/
├── staging/ # 仅 Staging 的代码和资源
├── prod/ # 仅 Prod 的代码和资源
├── debug/ # Debug 构建类型代码
└── release/ # Release 构建类型代码
多风味维度(例如,环境 + 层级):
android {
flavorDimensions += listOf("environment", "tier")
productFlavors {
create("dev") { dimension = "environment" }
create("prod") { dimension = "environment" }
create("free") { dimension = "tier" }
create("paid") { dimension = "tier" }
}
}
// 结果:devFreeDebug, devPaidDebug, prodFreeRelease 等。
| 类型 | 约定 | 示例 |
|---|---|---|
| 类/接口 | PascalCase | UserRepository, MainActivity |
| 函数/变量 | camelCase | getUserName(), isLoading |
| 常量 | SCREAMING_SNAKE | MAX_RETRY_COUNT |
| 包 | 小写 | com.example.myapp |
| 可组合函数 | PascalCase | @Composable fun UserCard() |
空安全 :
// ❌ 避免:非空断言 !!(可能导致崩溃)
val name = user!!.name
// ✅ 推荐:安全调用 + 默认值
val name = user?.name ?: "Unknown"
// ✅ 推荐:let 处理
user?.let { processUser(it) }
异常处理 :
// ❌ 避免:业务层随意 try-catch 吞掉异常
fun loadData() {
try {
val data = api.fetch()
} catch (e: Exception) {
// 吞掉异常,难以调试
}
}
// ✅ 推荐:让异常传播,在适当层级处理
suspend fun loadData(): Result<Data> {
return try {
Result.success(api.fetch())
} catch (e: Exception) {
Result.failure(e) // 包装并返回,让调用者决定处理方式
}
}
// ✅ 推荐:在 ViewModel 中统一处理
viewModelScope.launch {
runCatching { repository.loadData() }
.onSuccess { _uiState.value = UiState.Success(it) }
.onFailure { _uiState.value = UiState.Error(it.message) }
}
线程选择原则 :
| 操作类型 | 线程 | 描述 |
|---|---|---|
| UI 更新 | Dispatchers.Main | 更新 View、State、LiveData |
| 网络请求 | Dispatchers.IO | HTTP 调用、API 请求 |
| 文件 I/O | Dispatchers.IO | 本地存储、数据库操作 |
| 计算密集型 | Dispatchers.Default | JSON 解析、排序、加密 |
正确用法 :
// 在 ViewModel 中
viewModelScope.launch {
// 默认 Main 线程,可以更新 UI State
_uiState.value = UiState.Loading
// 切换到 IO 线程进行网络请求
val result = withContext(Dispatchers.IO) {
repository.fetchData()
}
// 自动返回到 Main 线程,更新 UI
_uiState.value = UiState.Success(result)
}
// 在 Repository 中(suspend 函数应该是主线程安全的)
suspend fun fetchData(): Data = withContext(Dispatchers.IO) {
api.getData()
}
常见错误 :
// ❌ 错误:在 IO 线程更新 UI
viewModelScope.launch(Dispatchers.IO) {
val data = api.fetch()
_uiState.value = data // 崩溃或警告!
}
// ❌ 错误:在主线程执行耗时操作
viewModelScope.launch {
val data = api.fetch() // 阻塞主线程!ANR
}
// ✅ 正确:在 IO 上获取,在 Main 上更新
viewModelScope.launch {
val data = withContext(Dispatchers.IO) { api.fetch() }
_uiState.value = data
}
// 默认是 public,需要时显式声明
class UserRepository { // public
private val cache = mutableMapOf<String, User>() // 仅在类内可见
internal fun clearCache() {} // 仅在模块内可见
}
// data class 属性默认是 public,跨模块使用时需小心
data class User(
val id: String, // public
val name: String
)
// ❌ 错误:访问未初始化的 lateinit
class MyViewModel : ViewModel() {
lateinit var data: String
fun process() = data.length // 可能崩溃
}
// ✅ 正确:使用可空或默认值
class MyViewModel : ViewModel() {
var data: String? = null
fun process() = data?.length ?: 0
}
// ❌ 错误:在 lambda 中使用 return
list.forEach { item ->
if (item.isEmpty()) return // 从外层函数返回!
}
// ✅ 正确:使用 return@forEach
list.forEach { item ->
if (item.isEmpty()) return@forEach
}
// ❌ 错误:字段声明为非空(服务器可能不返回)
data class UserResponse(
val id: String = "",
val name: String = "",
val avatar: String = ""
)
// ✅ 正确:所有字段声明为可空
data class UserResponse(
@SerializedName("id")
val id: String? = null,
@SerializedName("name")
val name: String? = null,
@SerializedName("avatar")
val avatar: String? = null
)
// ❌ 错误:只添加 Observer,不移除
class MyView : View {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
activity?.lifecycle?.addObserver(this)
}
// 内存泄漏!
}
// ✅ 正确:成对添加和移除
class MyView : View {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
activity?.lifecycle?.addObserver(this)
}
override fun onDetachedFromWindow() {
activity?.lifecycle?.removeObserver(this)
super.onDetachedFromWindow()
}
}
import android.util.Log
// Info:正常流程中的关键检查点
Log.i(TAG, "loadData: started, userId = $userId")
// Warning:异常但可恢复的情况
Log.w(TAG, "loadData: cache miss, fallback to network")
// Error:失败/错误情况
Log.e(TAG, "loadData failed: ${error.message}")
| 级别 | 使用场景 |
|---|---|
i (Info) | 正常流程、方法入口、关键参数 |
w (Warning) | 可恢复的异常、降级处理、空返回 |
e (Error) | 请求失败、捕获的异常、不可恢复的错误 |
// ❌ 错误:从非 Composable 函数调用 Composable
fun showError(message: String) {
Text(message) // 编译错误!
}
// ✅ 正确:标记为 @Composable
@Composable
fun ErrorMessage(message: String) {
Text(message)
}
// ❌ 错误:在 LaunchedEffect 外使用 suspend
@Composable
fun MyScreen() {
val data = fetchData() // 错误!
}
// ✅ 正确:使用 LaunchedEffect
@Composable
fun MyScreen() {
var data by remember { mutableStateOf<Data?>(null) }
LaunchedEffect(Unit) {
data = fetchData()
}
}
// 基本状态
var count by remember { mutableStateOf(0) }
// 派生状态(避免冗余计算)
val isEven by remember { derivedStateOf { count % 2 == 0 } }
// 跨重组持久化(例如,滚动位置)
val scrollState = rememberScrollState()
// ViewModel 中的状态
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
}
// ❌ 错误:在 Composable 中创建对象(每次重组都会创建)
@Composable
fun MyScreen() {
val viewModel = MyViewModel() // 错误!
}
// ✅ 正确:使用 viewModel() 或 remember
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
// ...
}
必须提供多分辨率图标:
| 目录 | 尺寸 | 用途 |
|---|---|---|
| mipmap-mdpi | 48x48 | 基线 |
| mipmap-hdpi | 72x72 | 1.5x |
| mipmap-xhdpi | 96x96 | 2x |
| mipmap-xxhdpi | 144x144 | 3x |
| mipmap-xxxhdpi | 192x192 | 4x |
推荐:使用自适应图标(Android 8+):
<!-- res/mipmap-anydpi-v26/ic_launcher.xml -->
<adaptive-icon>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
| 类型 | 前缀 | 示例 |
|---|---|---|
| 布局 | layout_ | layout_main.xml |
| 图片 | ic_, img_, bg_ | ic_user.png |
| 颜色 | color_ | color_primary |
| 字符串 | - | app_name, btn_submit |
变量名、资源 ID、颜色、图标和 XML 元素不得使用 Android 保留字或系统资源名称。使用保留名称会导致构建错误或资源冲突。
需避免的常见保留名称 :
| 类别 | 保留名称(请勿使用) |
|---|---|
| 颜色 | background, foreground, transparent, white, black |
| 图标/可绘制对象 | icon, logo, image, drawable |
| 视图 | view, text, button, layout, container |
| 属性 | id, name, type, style, theme, color |
| 系统 | app, android, content, data, action |
示例 :
<!-- ❌ 错误:使用保留名称 -->
<color name="background">#FFFFFF</color>
<color name="icon">#000000</color>
<!-- ✅ 正确:添加前缀或特定命名 -->
<color name="app_background">#FFFFFF</color>
<color name="icon_primary">#000000</color>
// ❌ 错误:变量名与系统冲突
val icon = R.drawable.my_icon
val background = Color.White
// ✅ 正确:使用描述性名称
val appIcon = R.drawable.my_icon
val screenBackground = Color.White
<!-- ❌ 错误:可绘制对象名称冲突 -->
<ImageView android:src="@drawable/icon" />
<!-- ✅ 正确:添加前缀 -->
<ImageView android:src="@drawable/ic_home" />
| 错误关键词 | 原因 | 修复方法 |
|---|---|---|
Unresolved reference | 缺少导入或未定义 | 检查导入,验证依赖项 |
Type mismatch | 类型不兼容 | 检查参数类型,添加转换 |
Cannot access | 可见性问题 | 检查 public/private/internal |
@Composable invocations | Composable 上下文错误 | 确保调用者也是 @Composable |
Duplicate class | 依赖冲突 | 使用 ./gradlew dependencies 排查 |
AAPT: error | 资源文件错误 | 检查 XML 语法和资源引用 |
./gradlew clean assembleDebug# 清理并构建
./gradlew clean assembleDebug
# 查看依赖树(排查冲突)
./gradlew :app:dependencies
# 查看详细错误
./gradlew assembleDebug --stacktrace
# 刷新依赖
./gradlew --refresh-dependencies
审查 Android UI 文件是否符合 Material Design 3 指南和 Android 最佳实践。
| 原则 | 描述 |
|---|---|
| 个性化 | 基于用户偏好和壁纸的动态色彩 |
| 适应性 | 在所有屏幕尺寸和设备形态上响应式 |
| 表现力 | 具有个性的鲜明色彩和排版 |
| 无障碍 | 面向所有用户的包容性设计 |
最新的演进通过以下方式增加了情感驱动的用户体验:
关键决策:将视觉风格与应用类别和目标受众相匹配。
| 应用类别 | 视觉风格 | 关键特征 |
|---|---|---|
| 实用工具 | 极简主义 | 简洁、高效、中性色彩 |
| 金融/银行 | 专业可信 | 保守色彩,注重安全 |
| 健康/保健 | 平静自然 | 柔和色彩,有机形状 |
| 儿童(3-5岁) | 趣味简单 | 明亮色彩,大目标区域(56dp+) |
| 儿童(6-12岁) | 有趣吸引人 | 充满活力,游戏化反馈 |
| 社交/娱乐 | 富有表现力 | 品牌驱动,丰富手势 |
| 生产力 | 简洁专注 | 极简,高对比度 |
| 电子商务 | 转化导向 | 清晰的行动号召,易于浏览 |
详细风格配置请参阅设计风格指南。
| 元素 | 最小比率 |
|---|---|
| 正文文本 | 4.5:1 |
| 大文本(18sp+) | 3:1 |
| UI 组件 | 3:1 |
| 类型 | 尺寸 |
|---|---|
| 最小 | 48 × 48dp |
| 推荐(主要操作) | 56 × 56dp |
| 儿童应用 | 56dp+ |
| 目标间间距 | 最小 8dp |
| 标记 | 值 | 用途 |
|---|---|---|
| xs | 4dp | 图标内边距 |
| sm | 8dp | 紧凑间距 |
| md | 16dp | 默认内边距 |
| lg | 24dp | 区块间距 |
| xl | 32dp | 大间隙 |
| xxl | 48dp | 屏幕边距 |
| 类别 | 尺寸 |
|---|---|
| Display | 57sp, 45sp, 36sp |
| Headline | 32sp, 28sp, 24sp |
| Title | 22sp, 16sp, 14sp |
| Body | 16sp, 14sp, 12sp |
| Label | 14sp, 12sp, 11sp |
| 类型 | 时长 |
|---|---|
| 微动(涟漪) | 50-100ms |
| 短(简单) | 100-200ms |
| 中(展开/折叠) | 200-300ms |
| 长(复杂) | 300-500ms |
| 组件 | 高度 | 最小宽度 |
|---|---|---|
| 按钮 | 40dp | 64dp |
| FAB | 56dp | 56dp |
| 文本字段 | 56dp | 280dp |
| 应用栏 | 64dp | - |
| 底部导航 | 80dp | - |
注意 : 仅当用户明确要求测试时才添加测试依赖项。
一个经过良好测试的 Android 应用使用分层测试:用于逻辑的快速本地单元测试,用于 UI 和集成的仪器化测试,以及 Gradle 托管设备,可在任何机器(包括 CI)上可重复地运行模拟器。
在添加测试依赖项之前,检查项目现有版本以避免冲突:
gradle/libs.versions.toml — 如果存在,使用项目的版本目录风格添加测试依赖项build.gradle.kts 中已固定的依赖版本版本对齐规则 :
| 测试依赖项 | 必须对齐于 | 如何检查 |
|---|---|---|
kotlinx-coroutines-test | 项目的 kotlinx-coroutines-core 版本 | 在构建文件或版本目录中搜索 kotlinx-coroutines |
compose-ui-test-junit4 | 项目的 Compose BOM 或 compose-compiler | 在构建文件中搜索 compose-bom 或 compose.compiler |
espresso-* | 所有 Espresso 构件必须使用相同版本 | 在构建文件中搜索 espresso |
androidx.test:runner, rules, ext:junit | 应使用兼容的 AndroidX Test 版本 | 在构建文件中搜索 androidx.test |
mockk | 必须支持项目的 Kotlin 版本 | 检查根 build.gradle.kts 或版本目录中的 kotlin 版本 |
依赖项参考 — 仅添加你需要的组:
dependencies {
// --- 本地单元测试 (src/test/) ---
testImplementation("junit:junit:<version>") // 4.13.2+
testImplementation("org.robolectric:robolectric:<version>") // 4.16.1+
testImplementation("io.mockk:mockk:<version>") // 匹配 Kotlin 版本
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:<version>") // 匹配 coroutines-core
testImplementation("androidx.arch.core:core-testing:<version>") // LiveData 的 InstantTaskExecutorRule
testImplementation("app.cash.turbine:turbine:<version>") // Flow/StateFlow 测试
// --- 仪器化测试 (src/androidTest/) ---
androidTestImplementation("androidx.test.ext:junit:<version>")
androidTestImplementation("androidx.test:runner:<version>")
androidTestImplementation("androidx.test:rules:<version>")
androidTestImplementation("androidx.test.espresso:espresso-core:<version>")
androidTestImplementation("androidx.test.espresso:espresso-contrib:<version>") // RecyclerView, Drawer
androidTestImplementation("androidx.test.espresso:espresso-intents:<version>") // Intent 验证
androidTestImplementation("androidx.test.espresso:espresso-idling-resource:<version>")
androidTestImplementation("androidx.test.uiautomator:uiautomator:<version>")
// --- Compose UI 测试(仅当项目使用 Compose 时) ---
androidTestImplementation("androidx.compose.ui:ui-test-junit4") // 版本来自 Compose BOM
debugImplementation("androidx.compose.ui:ui-test-manifest") // createComposeRule 所需
}
注意 : 如果项目使用 Compose BOM,
ui-test-junit4和ui-test-manifest不需要显式版本 — BOM 会管理它们。
在 android 块中启用 Robolectric 资源支持:
android {
testOptions {
unitTests.isIncludeAndroidResources = true // Robolectric 所需
}
}
| 层级 | 位置 | 运行于 | 速度 | 用于 |
|---|---|---|---|---|
| 单元测试 (JUnit) | src/test/ | JVM | ~毫秒 | ViewModels、repos、mappers、validators |
| 单元测试 + Robolectric | src/test/ | JVM + 模拟 Android | ~100ms | 需要 Context、资源、SharedPrefs 的代码 |
| Compose UI(本地) | src/test/ | JVM + Robolectric | ~100ms | Composable 渲染与交互 |
| Espresso | src/androidTest/ | 设备/模拟器 | ~秒 | 基于 View 的 UI 流程、Intents、DB 集成 |
| Compose UI(设备) | src/androidTest/ | 设备/模拟器 | ~秒 | 具有真实渲染的完整 Compose UI 流程 |
| UI Automator | src/androidTest/ | 设备/模拟器 | ~秒 | 系统对话框、通知、多应用 |
| 托管设备 | src/androidTest/ | Gradle 托管的 AVD | ~分钟(首次运行) | CI、跨 API 级别的矩阵测试 |
详细示例、代码模式和 Gradle 托管设备配置请参阅测试。
# 本地单元测试(快速,无需模拟器)
./gradlew test # 所有模块
./gradlew :app:testDebugUnitTest # app 模块,debug 变体
# 单个测试类
./gradlew :app:testDebugUnitTest --tests "com.example.myapp.CounterViewModelTest"
# 仪器化测试(需要设备或托管设备)
./gradlew connectedDebugAndroidTest # 在已连接的设备上
./gradlew pixel6Api34DebugAndroidTest # 在托管设备上
# 两者一起
./gradlew test connectedDebugAndroidTest
# 带覆盖率报告的测试(JaCoCo)
./gradlew testDebugUnitTest jacocoTestReport
每周安装数
182
仓库
GitHub 星标数
5.5K
首次出现
8 天前
安全审计
安装于
opencode179
codex178
cursor177
gemini-cli176
github-copilot176
amp175
Before starting development, assess the current project state:
| Scenario | Characteristics | Approach |
|---|---|---|
| Empty Directory | No files present | Full initialization required, including Gradle Wrapper |
| Has Gradle Wrapper | gradlew and gradle/wrapper/ exist | Use ./gradlew directly for builds |
| Android Studio Project | Complete project structure, may lack wrapper | Check wrapper, run gradle wrapper if needed |
| Incomplete Project | Partial files present | Check missing files, complete configuration |
Key Principles :
./gradlew assembleDebug succeedsgradle.properties is missing, create it first and configure AndroidXMyApp/
├── gradle.properties # Configure AndroidX and other settings
├── settings.gradle.kts
├── build.gradle.kts # Root level
├── gradle/wrapper/
│ └── gradle-wrapper.properties
├── app/
│ ├── build.gradle.kts # Module level
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/com/example/myapp/
│ │ └── MainActivity.kt
│ └── res/
│ ├── values/
│ │ ├── strings.xml
│ │ ├── colors.xml
│ │ └── themes.xml
│ └── mipmap-*/ # App icons
# Required configuration
android.useAndroidX=true
android.enableJetifier=true
# Build optimization
org.gradle.parallel=true
kotlin.code.style=official
# JVM memory settings (adjust based on project size)
# Small projects: 2048m, Medium: 4096m, Large: 8192m+
# org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
Note : If you encounter
OutOfMemoryErrorduring build, increase-Xmxvalue. Large projects with many dependencies may require 8GB or more.
dependencies {
// Use BOM to manage Compose versions
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
// Activity & ViewModel
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
}
Product Flavors allow you to create different versions of your app (e.g., free/paid, dev/staging/prod).
Configuration in app/build.gradle.kts :
android {
// Define flavor dimensions
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
// Different config values per flavor
buildConfigField("String", "API_BASE_URL", "\"https://dev-api.example.com\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "true")
// Different resources
resValue("string", "app_name", "MyApp Dev")
}
create("staging") {
dimension = "environment"
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
buildConfigField("String", "API_BASE_URL", "\"https://staging-api.example.com\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "true")
resValue("string", "app_name", "MyApp Staging")
}
create("prod") {
dimension = "environment"
// No suffix for production
buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "false")
resValue("string", "app_name", "MyApp")
}
}
buildTypes {
debug {
isDebuggable = true
isMinifyEnabled = false
}
release {
isDebuggable = false
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
Build Variant Naming : {flavor}{BuildType} → e.g., devDebug, prodRelease
Gradle Build Commands :
# List all available build variants
./gradlew tasks --group="build"
# Build specific variant (flavor + buildType)
./gradlew assembleDevDebug # Dev flavor, Debug build
./gradlew assembleStagingDebug # Staging flavor, Debug build
./gradlew assembleProdRelease # Prod flavor, Release build
# Build all variants of a specific flavor
./gradlew assembleDev # All Dev variants (debug + release)
./gradlew assembleProd # All Prod variants
# Build all variants of a specific build type
./gradlew assembleDebug # All flavors, Debug build
./gradlew assembleRelease # All flavors, Release build
# Install specific variant to device
./gradlew installDevDebug
./gradlew installProdRelease
# Build and install in one command
./gradlew installDevDebug && adb shell am start -n com.example.myapp.dev/.MainActivity
Access BuildConfig in Code :
Note : Starting from AGP 8.0,
BuildConfigis no longer generated by default. You must explicitly enable it in yourbuild.gradle.kts:android { buildFeatures { buildConfig = true } }
// Use build config values in your code
val apiUrl = BuildConfig.API_BASE_URL
val isLoggingEnabled = BuildConfig.ENABLE_LOGGING
if (BuildConfig.DEBUG) {
// Debug-only code
}
Flavor-Specific Source Sets :
app/src/
├── main/ # Shared code for all flavors
├── dev/ # Dev-only code and resources
│ ├── java/
│ └── res/
├── staging/ # Staging-only code and resources
├── prod/ # Prod-only code and resources
├── debug/ # Debug build type code
└── release/ # Release build type code
Multiple Flavor Dimensions (e.g., environment + tier):
android {
flavorDimensions += listOf("environment", "tier")
productFlavors {
create("dev") { dimension = "environment" }
create("prod") { dimension = "environment" }
create("free") { dimension = "tier" }
create("paid") { dimension = "tier" }
}
}
// Results in: devFreeDebug, devPaidDebug, prodFreeRelease, etc.
| Type | Convention | Example |
|---|---|---|
| Class/Interface | PascalCase | UserRepository, MainActivity |
| Function/Variable | camelCase | getUserName(), isLoading |
| Constant | SCREAMING_SNAKE | MAX_RETRY_COUNT |
| Package | lowercase | com.example.myapp |
Null Safety :
// ❌ Avoid: Non-null assertion !! (may crash)
val name = user!!.name
// ✅ Recommended: Safe call + default value
val name = user?.name ?: "Unknown"
// ✅ Recommended: let handling
user?.let { processUser(it) }
Exception Handling :
// ❌ Avoid: Random try-catch in business layer swallowing exceptions
fun loadData() {
try {
val data = api.fetch()
} catch (e: Exception) {
// Swallowing exception, hard to debug
}
}
// ✅ Recommended: Let exceptions propagate, handle at appropriate layer
suspend fun loadData(): Result<Data> {
return try {
Result.success(api.fetch())
} catch (e: Exception) {
Result.failure(e) // Wrap and return, let caller decide handling
}
}
// ✅ Recommended: Unified handling in ViewModel
viewModelScope.launch {
runCatching { repository.loadData() }
.onSuccess { _uiState.value = UiState.Success(it) }
.onFailure { _uiState.value = UiState.Error(it.message) }
}
Thread Selection Principles :
| Operation Type | Thread | Description |
|---|---|---|
| UI Updates | Dispatchers.Main | Update View, State, LiveData |
| Network Requests | Dispatchers.IO | HTTP calls, API requests |
| File I/O | Dispatchers.IO | Local storage, database operations |
| Compute Intensive | Dispatchers.Default | JSON parsing, sorting, encryption |
Correct Usage :
// In ViewModel
viewModelScope.launch {
// Default Main thread, can update UI State
_uiState.value = UiState.Loading
// Switch to IO thread for network request
val result = withContext(Dispatchers.IO) {
repository.fetchData()
}
// Automatically returns to Main thread, update UI
_uiState.value = UiState.Success(result)
}
// In Repository (suspend functions should be main-safe)
suspend fun fetchData(): Data = withContext(Dispatchers.IO) {
api.getData()
}
Common Mistakes :
// ❌ Wrong: Updating UI on IO thread
viewModelScope.launch(Dispatchers.IO) {
val data = api.fetch()
_uiState.value = data // Crash or warning!
}
// ❌ Wrong: Executing time-consuming operation on Main thread
viewModelScope.launch {
val data = api.fetch() // Blocking main thread! ANR
}
// ✅ Correct: Fetch on IO, update on Main
viewModelScope.launch {
val data = withContext(Dispatchers.IO) { api.fetch() }
_uiState.value = data
}
// Default is public, declare explicitly when needed
class UserRepository { // public
private val cache = mutableMapOf<String, User>() // Visible only within class
internal fun clearCache() {} // Visible only within module
}
// data class properties are public by default, be careful when used across modules
data class User(
val id: String, // public
val name: String
)
// ❌ Wrong: Accessing uninitialized lateinit
class MyViewModel : ViewModel() {
lateinit var data: String
fun process() = data.length // May crash
}
// ✅ Correct: Use nullable or default value
class MyViewModel : ViewModel() {
var data: String? = null
fun process() = data?.length ?: 0
}
// ❌ Wrong: Using return in lambda
list.forEach { item ->
if (item.isEmpty()) return // Returns from outer function!
}
// ✅ Correct: Use return@forEach
list.forEach { item ->
if (item.isEmpty()) return@forEach
}
// ❌ Wrong: Fields declared as non-null (server may not return them)
data class UserResponse(
val id: String = "",
val name: String = "",
val avatar: String = ""
)
// ✅ Correct: All fields declared as nullable
data class UserResponse(
@SerializedName("id")
val id: String? = null,
@SerializedName("name")
val name: String? = null,
@SerializedName("avatar")
val avatar: String? = null
)
// ❌ Wrong: Only adding Observer, not removing
class MyView : View {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
activity?.lifecycle?.addObserver(this)
}
// Memory leak!
}
// ✅ Correct: Paired add and remove
class MyView : View {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
activity?.lifecycle?.addObserver(this)
}
override fun onDetachedFromWindow() {
activity?.lifecycle?.removeObserver(this)
super.onDetachedFromWindow()
}
}
import android.util.Log
// Info: Key checkpoints in normal flow
Log.i(TAG, "loadData: started, userId = $userId")
// Warning: Abnormal but recoverable situations
Log.w(TAG, "loadData: cache miss, fallback to network")
// Error: Failure/error situations
Log.e(TAG, "loadData failed: ${error.message}")
| Level | Use Case |
|---|---|
i (Info) | Normal flow, method entry, key parameters |
w (Warning) | Recoverable exceptions, fallback handling, null returns |
e (Error) | Request failures, caught exceptions, unrecoverable errors |
// ❌ Wrong: Calling Composable from non-Composable function
fun showError(message: String) {
Text(message) // Compile error!
}
// ✅ Correct: Mark as @Composable
@Composable
fun ErrorMessage(message: String) {
Text(message)
}
// ❌ Wrong: Using suspend outside LaunchedEffect
@Composable
fun MyScreen() {
val data = fetchData() // Error!
}
// ✅ Correct: Use LaunchedEffect
@Composable
fun MyScreen() {
var data by remember { mutableStateOf<Data?>(null) }
LaunchedEffect(Unit) {
data = fetchData()
}
}
// Basic State
var count by remember { mutableStateOf(0) }
// Derived State (avoid redundant computation)
val isEven by remember { derivedStateOf { count % 2 == 0 } }
// Persist across recomposition (e.g., scroll position)
val scrollState = rememberScrollState()
// State in ViewModel
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
}
// ❌ Wrong: Creating objects in Composable (created on every recomposition)
@Composable
fun MyScreen() {
val viewModel = MyViewModel() // Wrong!
}
// ✅ Correct: Use viewModel() or remember
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
// ...
}
Must provide multi-resolution icons:
| Directory | Size | Purpose |
|---|---|---|
| mipmap-mdpi | 48x48 | Baseline |
| mipmap-hdpi | 72x72 | 1.5x |
| mipmap-xhdpi | 96x96 | 2x |
| mipmap-xxhdpi | 144x144 | 3x |
| mipmap-xxxhdpi | 192x192 | 4x |
Recommended: Use Adaptive Icon (Android 8+):
<!-- res/mipmap-anydpi-v26/ic_launcher.xml -->
<adaptive-icon>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
| Type | Prefix | Example |
|---|---|---|
| Layout | layout_ | layout_main.xml |
| Image | ic_, img_, bg_ | ic_user.png |
| Color | color_ | color_primary |
| String | - | app_name, btn_submit |
Variable names, resource IDs, colors, icons, and XML elements must not use Android reserved words or system resource names. Using reserved names causes build errors or resource conflicts.
Common Reserved Names to Avoid :
| Category | Reserved Names (Do NOT Use) |
|---|---|
| Colors | background, foreground, transparent, white, black |
| Icons/Drawables | icon, logo, image, drawable |
Examples :
<!-- ❌ Wrong: Using reserved names -->
<color name="background">#FFFFFF</color>
<color name="icon">#000000</color>
<!-- ✅ Correct: Add prefix or specific naming -->
<color name="app_background">#FFFFFF</color>
<color name="icon_primary">#000000</color>
// ❌ Wrong: Variable names conflict with system
val icon = R.drawable.my_icon
val background = Color.White
// ✅ Correct: Use descriptive names
val appIcon = R.drawable.my_icon
val screenBackground = Color.White
<!-- ❌ Wrong: Drawable name conflicts -->
<ImageView android:src="@drawable/icon" />
<!-- ✅ Correct: Add prefix -->
<ImageView android:src="@drawable/ic_home" />
| Error Keyword | Cause | Fix |
|---|---|---|
Unresolved reference | Missing import or undefined | Check imports, verify dependencies |
Type mismatch | Type incompatibility | Check parameter types, add conversion |
Cannot access | Visibility issue | Check public/private/internal |
@Composable invocations | Composable context error | Ensure caller is also @Composable |
Duplicate class |
./gradlew clean assembleDebug# Clean and build
./gradlew clean assembleDebug
# View dependency tree (investigate conflicts)
./gradlew :app:dependencies
# View detailed errors
./gradlew assembleDebug --stacktrace
# Refresh dependencies
./gradlew --refresh-dependencies
Review Android UI files for compliance with Material Design 3 Guidelines and Android best practices.
| Principle | Description |
|---|---|
| Personal | Dynamic color based on user preferences and wallpaper |
| Adaptive | Responsive across all screen sizes and form factors |
| Expressive | Bold colors and typography with personality |
| Accessible | Inclusive design for all users |
The latest evolution adds emotion-driven UX through:
Critical Decision : Match visual style to app category and target audience.
| App Category | Visual Style | Key Characteristics |
|---|---|---|
| Utility/Tool | Minimalist | Clean, efficient, neutral colors |
| Finance/Banking | Professional Trust | Conservative colors, security-focused |
| Health/Wellness | Calm & Natural | Soft colors, organic shapes |
| Kids (3-5) | Playful Simple | Bright colors, large targets (56dp+) |
| Kids (6-12) | Fun & Engaging | Vibrant, gamified feedback |
| Social/Entertainment | Expressive | Brand-driven, gesture-rich |
| Productivity | Clean & Focused | Minimal, high contrast |
| E-commerce | Conversion-focused | Clear CTAs, scannable |
See Design Style Guide for detailed style profiles.
| Element | Minimum Ratio |
|---|---|
| Body text | 4.5:1 |
| Large text (18sp+) | 3:1 |
| UI components | 3:1 |
| Type | Size |
|---|---|
| Minimum | 48 × 48dp |
| Recommended (primary actions) | 56 × 56dp |
| Kids apps | 56dp+ |
| Spacing between targets | 8dp minimum |
| Token | Value | Usage |
|---|---|---|
| xs | 4dp | Icon padding |
| sm | 8dp | Tight spacing |
| md | 16dp | Default padding |
| lg | 24dp | Section spacing |
| xl | 32dp | Large gaps |
| xxl | 48dp | Screen margins |
| Category | Sizes |
|---|---|
| Display | 57sp, 45sp, 36sp |
| Headline | 32sp, 28sp, 24sp |
| Title | 22sp, 16sp, 14sp |
| Body | 16sp, 14sp, 12sp |
| Label | 14sp, 12sp, 11sp |
| Type | Duration |
|---|---|
| Micro (ripples) | 50-100ms |
| Short (simple) | 100-200ms |
| Medium (expand/collapse) | 200-300ms |
| Long (complex) | 300-500ms |
| Component | Height | Min Width |
|---|---|---|
| Button | 40dp | 64dp |
| FAB | 56dp | 56dp |
| Text Field | 56dp | 280dp |
| App Bar | 64dp | - |
| Bottom Nav | 80dp | - |
| Topic | Reference |
|---|---|
| Colors, Typography, Spacing, Shapes | Visual Design |
| Animation & Transitions | Motion System |
| Accessibility Guidelines | Accessibility |
| Large Screens & Foldables | Adaptive Screens |
| Android Vitals & Performance | Performance & Stability |
| Privacy & Security | Privacy & Security |
| Audio, Video, Notifications | Functional Requirements |
Note : Only add test dependencies when the user explicitly asks for testing.
A well-tested Android app uses layered testing: fast local unit tests for logic, instrumentation tests for UI and integration, and Gradle Managed Devices to run emulators reproducibly on any machine — including CI.
Before adding test dependencies, inspect the project's existing versions to avoid conflicts:
gradle/libs.versions.toml — if present, add test deps using the project's version catalog stylebuild.gradle.kts for already-pinned dependency versionsVersion Alignment Rules :
| Test Dependency | Must Align With | How to Check |
|---|---|---|
kotlinx-coroutines-test | Project's kotlinx-coroutines-core version | Search for kotlinx-coroutines in build files or version catalog |
compose-ui-test-junit4 | Project's Compose BOM or compose-compiler | Search for compose-bom or compose.compiler in build files |
Dependencies Reference — add only the groups you need:
dependencies {
// --- Local unit tests (src/test/) ---
testImplementation("junit:junit:<version>") // 4.13.2+
testImplementation("org.robolectric:robolectric:<version>") // 4.16.1+
testImplementation("io.mockk:mockk:<version>") // match Kotlin version
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:<version>") // match coroutines-core
testImplementation("androidx.arch.core:core-testing:<version>") // InstantTaskExecutorRule for LiveData
testImplementation("app.cash.turbine:turbine:<version>") // Flow/StateFlow testing
// --- Instrumentation tests (src/androidTest/) ---
androidTestImplementation("androidx.test.ext:junit:<version>")
androidTestImplementation("androidx.test:runner:<version>")
androidTestImplementation("androidx.test:rules:<version>")
androidTestImplementation("androidx.test.espresso:espresso-core:<version>")
androidTestImplementation("androidx.test.espresso:espresso-contrib:<version>") // RecyclerView, Drawer
androidTestImplementation("androidx.test.espresso:espresso-intents:<version>") // Intent verification
androidTestImplementation("androidx.test.espresso:espresso-idling-resource:<version>")
androidTestImplementation("androidx.test.uiautomator:uiautomator:<version>")
// --- Compose UI tests (only if project uses Compose) ---
androidTestImplementation("androidx.compose.ui:ui-test-junit4") // version from Compose BOM
debugImplementation("androidx.compose.ui:ui-test-manifest") // required for createComposeRule
}
Note : If the project uses a Compose BOM,
ui-test-junit4andui-test-manifestdon't need explicit versions — the BOM manages them.
Enable Robolectric resource support in the android block:
android {
testOptions {
unitTests.isIncludeAndroidResources = true // required for Robolectric
}
}
| Layer | Location | Runs On | Speed | Use For |
|---|---|---|---|---|
| Unit (JUnit) | src/test/ | JVM | ~ms | ViewModels, repos, mappers, validators |
| Unit + Robolectric | src/test/ | JVM + simulated Android | ~100ms | Code needing Context, resources, SharedPrefs |
| Compose UI (local) | src/test/ | JVM + Robolectric | ~100ms | Composable rendering & interaction |
| Espresso | src/androidTest/ |
See Testing for detailed examples, code patterns, and Gradle Managed Device configuration.
# Local unit tests (fast, no emulator)
./gradlew test # all modules
./gradlew :app:testDebugUnitTest # app module, debug variant
# Single test class
./gradlew :app:testDebugUnitTest --tests "com.example.myapp.CounterViewModelTest"
# Instrumentation tests (requires device or managed device)
./gradlew connectedDebugAndroidTest # on connected device
./gradlew pixel6Api34DebugAndroidTest # on managed device
# Both together
./gradlew test connectedDebugAndroidTest
# Test with coverage report (JaCoCo)
./gradlew testDebugUnitTest jacocoTestReport
Weekly Installs
182
Repository
GitHub Stars
5.5K
First Seen
8 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode179
codex178
cursor177
gemini-cli176
github-copilot176
amp175
Kotlin Exposed ORM 模式指南:DSL查询、DAO、事务管理与生产配置
983 周安装
Google Workspace CLI 每周工作摘要:自动生成本周会议与未读邮件统计报告
7,900 周安装
Google Workspace CLI gws-chat 命令:管理 Google Chat 空间、表情符号与媒体文件
8,000 周安装
Sentry CLI 使用指南:命令行工具与Sentry监控平台交互的最佳实践
10,000 周安装
Vercel CLI 令牌认证部署指南 - 无需登录的自动化项目部署
9,900 周安装
Next.js 自动升级工具 - 安全高效升级 Next.js 版本,遵循官方迁移指南
10,000 周安装
Firestore Enterprise Native Mode 完整指南:配置、数据模型、安全规则与SDK使用
10,800 周安装
| Composable | PascalCase | @Composable fun UserCard() |
| Views | view, text, button, layout, container |
| Attributes | id, name, type, style, theme, color |
| System | app, android, content, data, action |
| Dependency conflict |
Use ./gradlew dependencies to investigate |
AAPT: error | Resource file error | Check XML syntax and resource references |
| App Style by Category | Design Style Guide |
espresso-* |
| All Espresso artifacts must use the same version |
Search for espresso in build files |
androidx.test:runner, rules, ext:junit | Should use compatible AndroidX Test versions | Search for androidx.test in build files |
mockk | Must support the project's Kotlin version | Check kotlin version in root build.gradle.kts or version catalog |
| Device/Emulator |
| ~seconds |
| View-based UI flows, Intents, DB integration |
| Compose UI (device) | src/androidTest/ | Device/Emulator | ~seconds | Full Compose UI flows with real rendering |
| UI Automator | src/androidTest/ | Device/Emulator | ~seconds | System dialogs, notifications, multi-app |
| Managed Device | src/androidTest/ | Gradle-managed AVD | ~minutes (first run) | CI, matrix testing across API levels |