Android kotlin compose
Skill and3r817/dot-claude-plugins/android-kotlin-compose/skills/android-kotlin-compose
A small collection of plugins I use with Claude Code
npx -y skills add and3r817/dot-claude-plugins --skill android-kotlin-composeAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Android development with Kotlin and Jetpack Compose. Use when user mentions "Compose", "Jetpack Compose", "Material3", "Hilt", "Room", "ViewModel", or needs to build Android UI, implement MVVM architecture, manage Compose state, or integrate Jetpack libraries (Navigation, Room, Hilt, ViewModel). Triggers on Android/Compose/Kotlin development tasks.
SKILL.md
14.7 KB, as published. Nobody here has run it
Android Kotlin Compose
Expert guidance for Android development using Kotlin and Jetpack Compose with modern architecture patterns.
When to Use This Skill
Invoke this skill when:
- Building Android UIs with Jetpack Compose
- Implementing MVVM architecture with ViewModels and StateFlow
- Managing state in Compose applications (remember, State hoisting)
- Integrating Jetpack libraries (Navigation, Room, Hilt, ViewModel, WorkManager)
- Designing Material3 interfaces and theming
- User explicitly mentions "Compose", "Kotlin Android", "Jetpack", or related libraries
Core Principles
- Compose-First UI — Declarative UI with composable functions
- Unidirectional Data Flow — State flows down, events flow up
- Single Source of Truth — ViewModel owns UI state
- Separation of Concerns — UI layer decoupled from data layer
Quick Reference
Project Structure (Recommended)
app/src/main/java/com/example/app/
├── di/ # Hilt modules
│ └── AppModule.kt
├── data/
│ ├── local/ # Room database
│ │ ├── dao/
│ │ ├── entity/
│ │ └── AppDatabase.kt
│ ├── remote/ # Network layer
│ │ ├── api/
│ │ └── dto/
│ └── repository/ # Repository implementations
├── domain/
│ ├── model/ # Domain models
│ ├── repository/ # Repository interfaces
│ └── usecase/ # Business logic
├── ui/
│ ├── components/ # Reusable composables
│ ├── theme/ # Material3 theming
│ │ ├── Color.kt
│ │ ├── Type.kt
│ │ └── Theme.kt
│ ├── navigation/ # Navigation graph
│ └── screens/ # Feature screens
│ └── feature/
│ ├── FeatureScreen.kt
│ └── FeatureViewModel.kt
└── App.kt # Application class
UI Development Patterns
Screen Pattern with ViewModel
// FeatureViewModel.kt
@HiltViewModel
class FeatureViewModel @Inject constructor(
private val repository: FeatureRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(FeatureUiState())
val uiState: StateFlow<FeatureUiState> = _uiState.asStateFlow()
fun onAction(action: FeatureAction) {
when (action) {
is FeatureAction.LoadData -> loadData()
is FeatureAction.UpdateItem -> updateItem(action.item)
}
}
private fun loadData() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
repository.getData()
.onSuccess { data ->
_uiState.update { it.copy(data = data, isLoading = false) }
}
.onFailure { error ->
_uiState.update { it.copy(error = error.message, isLoading = false) }
}
}
}
}
// FeatureUiState.kt
data class FeatureUiState(
val data: List<Item> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
sealed interface FeatureAction {
data object LoadData : FeatureAction
data class UpdateItem(val item: Item) : FeatureAction
}
// FeatureScreen.kt
@Composable
fun FeatureScreen(
viewModel: FeatureViewModel = hiltViewModel(),
onNavigateToDetail: (String) -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
FeatureContent(
uiState = uiState,
onAction = viewModel::onAction,
onNavigateToDetail = onNavigateToDetail
)
}
@Composable
private fun FeatureContent(
uiState: FeatureUiState,
onAction: (FeatureAction) -> Unit,
onNavigateToDetail: (String) -> Unit
) {
// Stateless composable - easy to preview and test
when {
uiState.isLoading -> LoadingIndicator()
uiState.error != null -> ErrorMessage(uiState.error)
else -> ItemList(
items = uiState.data,
onItemClick = { onNavigateToDetail(it.id) }
)
}
}
State Hoisting Pattern
// Stateful wrapper
@Composable
fun SearchBar(
onSearch: (String) -> Unit
) {
var query by rememberSaveable { mutableStateOf("") }
SearchBarContent(
query = query,
onQueryChange = { query = it },
onSearch = { onSearch(query) }
)
}
// Stateless implementation (previewable)
@Composable
private fun SearchBarContent(
query: String,
onQueryChange: (String) -> Unit,
onSearch: () -> Unit
) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
trailingIcon = {
IconButton(onClick = onSearch) {
Icon(Icons.Default.Search, contentDescription = "Search")
}
}
)
}
Side Effects
// LaunchedEffect - runs when key changes
LaunchedEffect(userId) {
viewModel.loadUser(userId)
}
// LaunchedEffect(Unit) - runs once on composition
LaunchedEffect(Unit) {
viewModel.initialize()
}
// DisposableEffect - cleanup when leaving composition
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
viewModel.refresh()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
// SideEffect - runs after every successful composition
SideEffect {
analytics.trackScreen("FeatureScreen")
}
// rememberCoroutineScope - for event handlers
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
viewModel.saveData()
}
}) { Text("Save") }
Navigation
Type-Safe Navigation (Compose 2.8+)
// Routes.kt
@Serializable
data object Home
@Serializable
data class Detail(val id: String)
@Serializable
data class Settings(val section: String? = null)
// NavGraph.kt
@Composable
fun AppNavGraph(
navController: NavHostController = rememberNavController()
) {
NavHost(navController = navController, startDestination = Home) {
composable<Home> {
HomeScreen(
onNavigateToDetail = { id ->
navController.navigate(Detail(id))
}
)
}
composable<Detail> { backStackEntry ->
val detail: Detail = backStackEntry.toRoute()
DetailScreen(itemId = detail.id)
}
composable<Settings> { backStackEntry ->
val settings: Settings = backStackEntry.toRoute()
SettingsScreen(initialSection = settings.section)
}
}
}
Navigation with Results
// Returning results to previous screen
composable<SelectItem> {
SelectItemScreen(
onItemSelected = { item ->
navController.previousBackStackEntry
?.savedStateHandle
?.set("selected_item", item)
navController.popBackStack()
}
)
}
// Receiving results
composable<Home> {
val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
val selectedItem by savedStateHandle
?.getStateFlow<Item?>("selected_item", null)
?.collectAsStateWithLifecycle() ?: remember { mutableStateOf(null) }
HomeScreen(selectedItem = selectedItem)
}
Dependency Injection with Hilt
Module Setup
// AppModule.kt
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
}
// RepositoryModule.kt
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository
}
ViewModel Injection
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val userId: String = savedStateHandle.get<String>("userId")
?: throw IllegalArgumentException("userId required")
}
// In Composable
@Composable
fun UserScreen(
viewModel: UserViewModel = hiltViewModel()
) {
// ViewModel automatically scoped to navigation destination
}
Room Database
Entity and DAO
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: String,
val name: String,
val email: String,
@ColumnInfo(name = "created_at") val createdAt: Long
)
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
fun getUser(id: String): Flow<UserEntity?>
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsers(): Flow<List<UserEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: UserEntity)
@Delete
suspend fun deleteUser(user: UserEntity)
@Transaction
@Query("SELECT * FROM users WHERE id = :id")
fun getUserWithPosts(id: String): Flow<UserWithPosts>
}
@Database(
entities = [UserEntity::class, PostEntity::class],
version = 1,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
}
Repository Pattern
interface UserRepository {
fun getUser(id: String): Flow<User?>
fun getAllUsers(): Flow<List<User>>
suspend fun saveUser(user: User): Result<Unit>
}
class UserRepositoryImpl @Inject constructor(
private val userDao: UserDao,
private val userApi: UserApi,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : UserRepository {
override fun getUser(id: String): Flow<User?> {
return userDao.getUser(id)
.map { it?.toDomain() }
.flowOn(ioDispatcher)
}
override suspend fun saveUser(user: User): Result<Unit> {
return withContext(ioDispatcher) {
runCatching {
userDao.insertUser(user.toEntity())
}
}
}
}
Material3 Theming
Theme Setup
// Color.kt
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Purple40 = Color(0xFF6650a4)
// Type.kt
val Typography = Typography(
headlineLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
lineHeight = 40.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp
)
)
// Theme.kt
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
Using Theme Values
@Composable
fun ThemedCard() {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Text(
text = "Title",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Common Anti-Patterns
❌ Avoid
// DON'T: State in composable that survives recomposition incorrectly
@Composable
fun Counter() {
var count = 0 // Resets on every recomposition!
Button(onClick = { count++ }) { Text("$count") }
}
// DON'T: Side effects in composition
@Composable
fun BadScreen(viewModel: ViewModel) {
viewModel.loadData() // Called on every recomposition!
}
// DON'T: Create objects in composition without remember
@Composable
fun BadList(items: List<Item>) {
val derivedList = items.filter { it.isActive } // Recreated every recomposition
}
// DON'T: Collect flow without lifecycle awareness
val state by viewModel.uiState.collectAsState() // May leak
✅ Correct
// DO: Use remember for state
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}
// DO: Use LaunchedEffect for side effects
@Composable
fun GoodScreen(viewModel: ViewModel) {
LaunchedEffect(Unit) {
viewModel.loadData()
}
}
// DO: Use remember/derivedStateOf for computed values
@Composable
fun GoodList(items: List<Item>) {
val derivedList by remember(items) {
derivedStateOf { items.filter { it.isActive } }
}
}
// DO: Use lifecycle-aware collection
val state by viewModel.uiState.collectAsStateWithLifecycle()
Resources
For detailed patterns and integration guides, see:
references/compose-patterns.md— Advanced composition patterns, performance optimization, custom layoutsreferences/jetpack-integration.md— Deep integration with Navigation, Room, Hilt, WorkManagerreferences/animations.md— Complete animation guide: animate*AsState, transitions, AnimatedVisibility, gestures, shared elements, Lottie
When to Consult References
- Complex custom layouts →
compose-patterns.md - Multi-module navigation setup →
jetpack-integration.md - Database migrations or relations →
jetpack-integration.md - Performance profiling →
compose-patterns.md - Animations (state-based, transitions, gestures, shared elements) →
animations.md - Lottie integration →
animations.md