用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/JohnNuwan/EVA_CORE --skill kotlin-android命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | kotlin-android |
| description | Développement Android natif avec Kotlin — Jetpack Compose, Gradle, Material Design 3, Android SDK, Play Store |
Développement d'applications Android natives avec Kotlin, couvrant Jetpack Compose, Material Design 3, les Android Jetpack Libraries, Gradle build system, et le déploiement Play Store.
// Coroutines & Flow
class UserRepository(private val api: UserApi) {
fun fetchUsers(): Flow<List<User>> = flow {
emit(api.getUsers())
}.flowOn(Dispatchers.IO)
suspend fun saveUser(user: User) = withContext(Dispatchers.IO) {
api.createUser(user)
}
}
// Sealed classes pour états
sealed interface UiState<out T> {
data object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
data class Error(val message: String, val exception: Throwable? = null) : UiState<Nothing>
}
// Extension functions
fun String.isValidEmail(): Boolean = matches(Regex("^[A-Za-z0-9+_.-]+@(.+)$"))
// Type-safe builders (DSL)
fun Context.showToast(message: String) =
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
viewModelScope, lifecycleScope, custom scopesMain, IO, Default, UnconfinedstateFlow, sharedFlow, callbackFlow, channelFlowChannel<T>, BroadcastChannel, ConflatedBroadcastChannelcoroutineScope, supervisorScope, Jobtry/catch, catch {} operator, CoroutineExceptionHandlerby lazy, by Delegates.observable, by map@JvmInline value class Email(val value: String)context(Context) pour l'injection@Contract pour l'inférence de smart castexpect/actual pour KMP@Serializable, kotlinx.serialization vs Gson/Moshi@Composable
fun UserCard(
user: User,
onUserClick: (User) -> Unit,
modifier: Modifier = Modifier
) {
Card(
onClick = { onUserClick(user) },
modifier = modifier.fillMaxWidth().padding(8.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
AsyncImage(
model = user.avatarUrl,
contentDescription = "Avatar",
modifier = Modifier.size(48.dp).clip(CircleShape)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(text = user.name, style = MaterialTheme.typography.titleMedium)
Text(text = user.email, style = MaterialTheme.typography.bodySmall)
}
}
}
}
// ViewModel avec StateFlow
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<User>>> = _uiState.asStateFlow()
init { loadUsers() }
fun loadUsers() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val users = repository.fetchUsers()
_uiState.value = UiState.Success(users)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Erreur inconnue")
}
}
}
}
// Collect dans Compose
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
when (val s = state) {
is UiState.Loading -> Box(modifier = Modifier.fillMaxSize()) { CircularProgressIndicator() }
is UiState.Success -> UserList(s.data)
is UiState.Error -> ErrorMessage(s.message)
}
}
MaterialTheme, ColorScheme, Typography, ShapesHorizontalPager, VerticalPager (HorizontalPager 1.5+)fadeIn, slideInVerticallySizeTransformanimateFloatAsState, animateColorAsStatetween, spring, keyframes, repeatableupdateTransition pour animations multi-étatsSharedTransitionLayout (Compose 1.7+)// Navigation Compose
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable(
route = "user/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.IntType })
) { backStackEntry ->
val userId = backStackEntry.arguments?.getInt("userId") ?: return@composable
UserDetailScreen(userId = userId)
}
}
// Deep links
composable(
route = "profile/{id}",
deepLinks = listOf(navDeepLink { uriPattern = "myapp://profile/{id}" })
)
@Entity, @Dao, @Database, @Query, @Insert, @TransactionPreferencesDataStore, ProtoDataStore (remplace SharedPreferences)FileProvider.getUriForFile pour partage de fichiers| Component | Usage | Scope |
|---|---|---|
| ViewModel | State persistant rotation | Activity/Fragment |
| LiveData | Observable data (legacy) | ViewModel |
| Room | DB relationnelle | Repository |
| WorkManager | Tâches différées, périodiques | Application |
| SavedStateHandle | Survie à la destruction | ViewModel |
| Hilt/Dagger | Injection de dépendances | Application |
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = try {
syncData()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
// Periodic work (minimum interval: 15 min)
val request = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS)
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork("sync", ExistingPeriodicWorkPolicy.KEEP, request)
[versions]
compose-bom = "2024.12.01"
room = "2.6.1"
hilt = "2.51"
[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
[plugins]
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
}
android {
namespace = "com.example.app"
compileSdk = 35
defaultConfig {
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0.0"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.bundles.compose)
implementation(libs.bundles.hilt)
ksp(libs.bundles.hilt.compiler)
}
@GET, @POST, @Path, @Query, @Body, Converter.Factory@HiltAndroidApp, @AndroidEntryPoint, @HiltViewModel, @Module, @Providessingle, factory, scoped@Component, @Module, @Inject, @ScopeAsyncImage, ImageLoader, disk cache LRURequestBuilder, RequestOptions, transformations// Dynamic colors Android 12+
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicLightColorScheme(context)
} else {
lightColorScheme(
primary = Color(0xFF6750A4),
onPrimary = Color.White,
primaryContainer = Color(0xFFEADDFF),
secondary = Color(0xFF625B71),
surface = Color(0xFFFFFBFE)
)
}
CenterAlignedTopAppBar, LargeTopAppBarModalBottomSheet, BottomSheetScaffoldDockedSearchBar, ExpandedSearchBarclass UserViewModelTest {
private val repository: UserRepository = mockk()
private lateinit var viewModel: UserViewModel
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
viewModel = UserViewModel(repository)
}
@Test
fun `loadUsers emits success state`() = runTest {
coEvery { repository.fetchUsers() } returns listOf(mockUser())
viewModel.loadUsers()
val state = viewModel.uiState.first { it is UiState.Success }
assertInstanceOf(UiState.Success::class.java, state)
}
}
@Test
fun testUserCardClick() {
composeTestRule.setContent {
UserCard(user = testUser, onUserClick = { /* assert */ })
}
composeTestRule.onNodeWithText("John Doe").assertIsDisplayed()
composeTestRule.onNodeWithTag("user_card").performClick()
}
captureRoboImage avec Compose# Keystore
keytool -genkey -v -keystore release.keystore -alias app -keyalg RSA -keysize 2048 -validity 10000
# Build APK/AAB
./gradlew bundleRelease # Android App Bundle
./gradlew assembleRelease # APK
ReviewManager, ReviewInfoAppUpdateManager, IMMEDIATE / FLEXIBLE- name: Build Release
run: ./gradlew bundleRelease
- name: Sign & Upload to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJson: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
packageName: com.example.app
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: internal
collectAsState() dans parent scoperememberSaveableMediaStore, SAF, MANAGE_EXTERNAL_STORAGE déprécié@RequiresApi, Build.VERSION.SDK_INT checks-keep class pour réflexion/DI, @Keep annotation