android-development
3
总安装量
3
周安装量
#62639
全站排名
安装命令
npx skills add https://github.com/wzcxbg/develop-agent-skills --skill android-development
Agent 安装分布
trae
3
mcpjam
1
claude-code
1
junie
1
windsurf
1
zencoder
1
Skill 文档
Android Development Skill
Project Structure
project/
âââ app/
â âââ src/
â â âââ main/
â â â âââ kotlin/com/example/app/
â â â â âââ data/ # Data layer
â â â â â âââ local/ # Room database
â â â â â âââ remote/ # Retrofit/Ktor services
â â â â â âââ repository/ # Repository implementations
â â â â âââ di/ # Koin modules
â â â â âââ domain/ # Business logic
â â â â â âââ model/ # Domain models
â â â â â âââ repository/ # Repository interfaces
â â â â â âââ usecase/ # Use cases
â â â â âââ ui/ # Presentation layer
â â â â â âââ feature/ # Feature screens
â â â â â â âââ FeatureScreen.kt # Compose UI
â â â â â â âââ FeatureViewModel.kt
â â â â â âââ components/ # Reusable Compose components
â â â â â âââ theme/ # Material theme
â â â â âââ App.kt # Application class
â â â âââ res/
â â â âââ AndroidManifest.xml
â â âââ test/ # Unit tests
â â âââ androidTest/ # Instrumentation tests
â âââ build.gradle.kts
âââ build.gradle.kts # Project-level build file
âââ gradle.properties
âââ gradle/
â âââ libs.versions.toml # Version Catalog
âââ settings.gradle.kts
âââ CLAUDE.md
Gradle Configuration (Kotlin DSL & Version Catalog)
Version Catalog (gradle/libs.versions.toml)
[versions]
agp = "8.3.0"
kotlin = "1.9.22"
coreKtx = "1.12.0"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.7.0"
activityCompose = "1.8.2"
composeBom = "2024.02.00"
ksp = "1.9.22-1.0.17"
room = "2.6.1"
retrofit = "2.9.0"
okhttp = "4.12.0"
koin = "3.5.3"
coil = "2.6.0"
serialization = "1.6.3"
workManager = "2.9.0"
navigation = "2.7.7" # Navigation 3 reference likely refers to new Compose Navigation patterns or alpha versions, using stable 2.7.7 for now or keeping user intent if specific library exists. Assuming standard navigation-compose here mapped to user's 'navigation3' alias for structure consistency.
preference = "1.2.1"
media3 = "1.3.0"
jsoup = "1.17.2"
zxing = "3.5.3"
json = "20231013"
coroutinesTest = "1.7.3"
mockk = "1.13.8"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
squareup-retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
squareup-retrofit2-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
squareup-okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager" }
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference" }
# Mapping user's 'navigation3' to standard navigation-compose for stability, or update if specific library intended
androidx-navigation3-runtime = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigation" }
androidx-navigation3-ui = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" }
androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
google-gson = { group = "com.google.code.gson", name = "gson", version = "2.10.1" }
zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoup" }
androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
koin-navigation3 = { group = "io.insert-koin", name = "koin-androidx-navigation", version.ref = "koin" }
koin-androidx-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" }
koin-androidx-startup = { group = "io.insert-koin", name = "koin-androidx-startup", version.ref = "koin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
org-json = { group = "org.json", name = "json", version.ref = "json" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutinesTest" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
App-level build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
}
android {
namespace = "com.example.app"
compileSdk {
version = release(36)
}
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
allWarningsAsErrors.set(true)
freeCompilerArgs.addAll(
"-opt-in=org.koin.core.annotation.KoinExperimentalAPI",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
)
}
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
// Room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
// Network
implementation(libs.squareup.retrofit2)
implementation(libs.squareup.retrofit2.converter.gson)
implementation(libs.squareup.okhttp3.logging.interceptor)
implementation(libs.google.gson)
implementation(libs.kotlinx.serialization.json)
// WorkManager & Preference
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.preference.ktx)
// Navigation
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
// Image & Utils
implementation(libs.coil.compose)
implementation(libs.zxing)
implementation(libs.jsoup)
// Media
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
// Koin
implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
implementation(libs.koin.navigation3)
implementation(libs.koin.androidx.workmanager)
implementation(libs.koin.androidx.startup)
// Testing
testImplementation(libs.junit)
testImplementation(libs.org.json)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
testImplementation(libs.androidx.room.testing)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}
Kotlin Coroutines & Flow
ViewModel with StateFlow
class UserViewModel(
private val getUserUseCase: GetUserUseCase,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
private val userId: String = checkNotNull(savedStateHandle["userId"])
init {
loadUser()
}
fun loadUser() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
getUserUseCase(userId)
.catch { e ->
_uiState.update {
it.copy(isLoading = false, error = e.message)
}
}
.collect { user ->
_uiState.update {
it.copy(isLoading = false, user = user, error = null)
}
}
}
}
fun clearError() {
_uiState.update { it.copy(error = null) }
}
}
data class UserUiState(
val user: User? = null,
val isLoading: Boolean = false,
val error: String? = null
)
Repository with Flow
interface UserRepository {
fun getUser(userId: String): Flow<User>
fun observeUsers(): Flow<List<User>>
suspend fun saveUser(user: User)
}
class UserRepositoryImpl(
private val api: UserApi,
private val dao: UserDao,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : UserRepository {
override fun getUser(userId: String): Flow<User> = flow {
// Emit cached data first
dao.getUserById(userId)?.let { emit(it) }
// Fetch from network and update cache
val remoteUser = api.getUser(userId)
dao.insert(remoteUser)
emit(remoteUser)
}.flowOn(dispatcher)
override fun observeUsers(): Flow<List<User>> =
dao.observeAllUsers().flowOn(dispatcher)
override suspend fun saveUser(user: User) = withContext(dispatcher) {
api.saveUser(user)
dao.insert(user)
}
}
Jetpack Compose
Screen with ViewModel
@Composable
fun UserScreen(
viewModel: UserViewModel = koinViewModel(),
onNavigateBack: () -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
UserScreenContent(
uiState = uiState,
onRefresh = viewModel::loadUser,
onErrorDismiss = viewModel::clearError,
onNavigateBack = onNavigateBack
)
}
@Composable
private fun UserScreenContent(
uiState: UserUiState,
onRefresh: () -> Unit,
onErrorDismiss: () -> Unit,
onNavigateBack: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("User Profile") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
}
}
)
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
when {
uiState.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
uiState.user != null -> {
UserContent(user = uiState.user)
}
}
uiState.error?.let { error ->
Snackbar(
modifier = Modifier.align(Alignment.BottomCenter),
action = {
TextButton(onClick = onErrorDismiss) {
Text("Dismiss")
}
}
) {
Text(error)
}
}
}
}
}
Sealed Classes for State
Koin Dependency Injection
Koin Modules
val appModule = module {
single<UserRepository> { UserRepositoryImpl(get(), get()) }
factory { GetUserUseCase(get()) }
viewModel { params -> UserViewModel(get(), params.get()) }
}
val networkModule = module {
single { Retrofit.Builder()...build().create(UserApi::class.java) }
}
Application Class
class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@App)
modules(appModule, networkModule)
}
}
}
Sealed Classes for State
Result Wrapper
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable) : Result<Nothing>
data object Loading : Result<Nothing>
}
fun <T> Result<T>.getOrNull(): T? = (this as? Result.Success)?.data
inline fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> = when (this) {
is Result.Success -> Result.Success(transform(data))
is Result.Error -> this
is Result.Loading -> this
}
Testing with MockK & Turbine
ViewModel Tests
@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val getUserUseCase: GetUserUseCase = mockk(relaxed = true)
private val savedStateHandle = SavedStateHandle(mapOf("userId" to "123"))
private lateinit var viewModel: UserViewModel
@Before
fun setup() {
// MockK setup
}
@Test
fun `loadUser success updates state with user`() = runTest {
val user = User("123", "John Doe", "john@example.com")
coEvery { getUserUseCase("123") } returns flowOf(user)
viewModel = UserViewModel(getUserUseCase, savedStateHandle)
viewModel.uiState.test {
val initial = awaitItem()
assertFalse(initial.isLoading)
viewModel.loadUser()
val loading = awaitItem()
assertTrue(loading.isLoading)
val success = awaitItem()
assertFalse(success.isLoading)
assertEquals(user, success.user)
}
}
}
class MainDispatcherRule(
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
Integration Test with Koin
class UserIntegrationTest : KoinTest {
private val userRepository: UserRepository by inject()
@get:Rule
val koinTestRule = KoinTestRule.create {
modules(appModule, networkModule)
}
@Test
fun `repository is injected correctly`() {
assertNotNull(userRepository)
}
}
GitHub Actions
name: Android Kotlin CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Run Detekt
run: ./gradlew detekt
- name: Run Ktlint
run: ./gradlew ktlintCheck
- name: Run Unit Tests
run: ./gradlew testDebugUnitTest
- name: Build Debug APK
run: ./gradlew assembleDebug
Lint Configuration
detekt.yml
build:
maxIssues: 0
complexity:
LongMethod:
threshold: 20
LongParameterList:
functionThreshold: 4
TooManyFunctions:
thresholdInFiles: 10
style:
MaxLineLength:
maxLineLength: 120
WildcardImport:
active: true
coroutines:
GlobalCoroutineUsage:
active: true
Kotlin Anti-Patterns
- â Blocking coroutines on Main – Never use
runBlockingon main thread - â GlobalScope usage – Use structured concurrency with viewModelScope/lifecycleScope
- â Collecting flows in init – Use
repeatOnLifecycleorcollectAsStateWithLifecycle - â Mutable state exposure – Expose
StateFlownotMutableStateFlow - â Not handling exceptions in flows – Always use
catchoperator - â Lateinit for nullable – Use
lazyor nullable with? - â Hardcoded dispatchers – Inject dispatchers for testability
- â Not using sealed classes – Prefer sealed for finite state sets
- â Side effects in Composables – Use
LaunchedEffect/SideEffect - â Unstable Compose parameters – Use stable/immutable types or
@Stable - â Not-null assertion (!!) – Never use
!!, always use safe call?., elvis operator?:, orrequireNotNull/checkNotNullwith message - â In-code OptIn annotations – Do not use
@OptIn(Experimental...)in code. ConfigurefreeCompilerArgsinbuild.gradle.ktsinstead.