Claude Code for Android: Kotlin, Jetpack Compose, and Modern Android Development — Claude Skills 360 Blog
Blog / Mobile / Claude Code for Android: Kotlin, Jetpack Compose, and Modern Android Development
Mobile

Claude Code for Android: Kotlin, Jetpack Compose, and Modern Android Development

Published: December 7, 2026
Read time: 9 min read
By: Claude Skills 360

Modern Android development uses Jetpack Compose for declarative UI, Kotlin Coroutines for async, ViewModel with StateFlow for state management, Room for local persistence, and Hilt for dependency injection. The architecture pattern is MVVM with a Repository layer — ViewModels expose StateFlow to Composables via collectAsStateWithLifecycle. Claude Code generates Compose screens, ViewModels, Room DAOs, Hilt modules, and WorkManager tasks for deferred background work.

CLAUDE.md for Android Projects

## Android Stack
- Kotlin 2.x, Android Gradle Plugin 8.x
- UI: Jetpack Compose (no XML layouts)
- Navigation: Navigation Compose with type-safe routes (Kotlin Serialization)
- State: ViewModel + StateFlow + collectAsStateWithLifecycle
- Network: Retrofit 2.x + OkHttp + Kotlin Serialization (no Gson)
- DB: Room 2.7+ with KSP (not KAPT)
- DI: Hilt (mandatory — no manual DI)
- Background: WorkManager for deferrable, guaranteed work
- Image: Coil 3 (Compose-native async image loading)
- Min SDK: 26 (Android 8.0) — covers 95%+ of devices

ViewModel with StateFlow

// ui/orders/OrdersViewModel.kt
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

data class OrdersUiState(
    val orders: List<Order> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null,
    val searchQuery: String = "",
)

sealed interface OrdersEvent {
    data class LoadOrders(val forceRefresh: Boolean = false) : OrdersEvent
    data class SearchQueryChanged(val query: String) : OrdersEvent
    data class CancelOrder(val orderId: String) : OrdersEvent
    object DismissError : OrdersEvent
}

@HiltViewModel
class OrdersViewModel @Inject constructor(
    private val orderRepository: OrderRepository,
) : ViewModel() {
    
    private val _uiState = MutableStateFlow(OrdersUiState(isLoading = true))
    val uiState: StateFlow<OrdersUiState> = _uiState.asStateFlow()
    
    init {
        observeOrders()
    }
    
    private fun observeOrders() {
        viewModelScope.launch {
            orderRepository.observeOrders()
                .catch { e ->
                    _uiState.update { it.copy(error = e.message, isLoading = false) }
                }
                .collect { orders ->
                    _uiState.update { it.copy(orders = orders, isLoading = false) }
                }
        }
    }
    
    fun onEvent(event: OrdersEvent) {
        when (event) {
            is OrdersEvent.LoadOrders -> loadOrders(event.forceRefresh)
            is OrdersEvent.SearchQueryChanged -> searchOrders(event.query)
            is OrdersEvent.CancelOrder -> cancelOrder(event.orderId)
            OrdersEvent.DismissError -> _uiState.update { it.copy(error = null) }
        }
    }
    
    private fun loadOrders(forceRefresh: Boolean) {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            orderRepository.refreshOrders(forceRefresh)
            _uiState.update { it.copy(isLoading = false) }
        }
    }
    
    private fun searchOrders(query: String) {
        _uiState.update { it.copy(searchQuery = query) }
        viewModelScope.launch {
            val filtered = orderRepository.searchOrders(query)
            _uiState.update { it.copy(orders = filtered) }
        }
    }
    
    private fun cancelOrder(orderId: String) {
        viewModelScope.launch {
            try {
                orderRepository.cancelOrder(orderId)
            } catch (e: Exception) {
                _uiState.update { it.copy(error = "Failed to cancel order: ${e.message}") }
            }
        }
    }
}

Jetpack Compose Screen

// ui/orders/OrdersScreen.kt
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle

@Composable
fun OrdersScreen(
    onOrderClick: (String) -> Unit,
    viewModel: OrdersViewModel = hiltViewModel(),
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Orders") },
                actions = {
                    IconButton(onClick = { onOrderClick("new") }) {
                        Icon(Icons.Default.Add, contentDescription = "New Order")
                    }
                }
            )
        },
        snackbarHost = {
            if (uiState.error != null) {
                LaunchedEffect(uiState.error) {
                    viewModel.onEvent(OrdersEvent.DismissError)
                }
            }
        }
    ) { paddingValues ->
        Column(modifier = Modifier.padding(paddingValues)) {
            // Search bar
            OutlinedTextField(
                value = uiState.searchQuery,
                onValueChange = { viewModel.onEvent(OrdersEvent.SearchQueryChanged(it)) },
                modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
                placeholder = { Text("Search orders...") },
                leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
                singleLine = true,
            )
            
            when {
                uiState.isLoading && uiState.orders.isEmpty -> {
                    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                        CircularProgressIndicator()
                    }
                }
                uiState.orders.isEmpty -> {
                    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                        Text("No orders found", style = MaterialTheme.typography.bodyLarge)
                    }
                }
                else -> {
                    LazyColumn(
                        contentPadding = PaddingValues(16.dp),
                        verticalArrangement = Arrangement.spacedBy(8.dp),
                    ) {
                        items(uiState.orders, key = { it.id }) { order ->
                            OrderCard(
                                order = order,
                                onClick = { onOrderClick(order.id) },
                                onCancel = { viewModel.onEvent(OrdersEvent.CancelOrder(order.id)) },
                                modifier = Modifier.animateItem(),
                            )
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun OrderCard(
    order: Order,
    onClick: () -> Unit,
    onCancel: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Card(
        onClick = onClick,
        modifier = modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
    ) {
        Row(
            modifier = Modifier.padding(16.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
        ) {
            Column {
                Text(
                    "#${order.id.takeLast(6)}",
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.Bold,
                )
                Text(
                    order.customerName,
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                )
            }
            Column(horizontalAlignment = Alignment.End) {
                Text(
                    formatCurrency(order.totalCents),
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.Bold,
                )
                StatusChip(status = order.status)
            }
        }
    }
}

Room Database

// data/local/OrderDao.kt
@Dao
interface OrderDao {
    @Query("SELECT * FROM orders ORDER BY created_at DESC")
    fun observeAll(): Flow<List<OrderEntity>>
    
    @Query("SELECT * FROM orders WHERE id = :id")
    suspend fun findById(id: String): OrderEntity?
    
    @Query("SELECT * FROM orders WHERE customer_name LIKE '%' || :query || '%' OR id LIKE '%' || :query || '%'")
    suspend fun search(query: String): List<OrderEntity>
    
    @Upsert
    suspend fun upsertAll(orders: List<OrderEntity>)
    
    @Delete
    suspend fun delete(order: OrderEntity)
    
    @Query("UPDATE orders SET status = :status WHERE id = :id")
    suspend fun updateStatus(id: String, status: String)
    
    @Transaction
    @Query("SELECT * FROM orders WHERE id = :id")
    fun observeOrderWithItems(id: String): Flow<OrderWithItems?>
}

@Entity(tableName = "orders")
data class OrderEntity(
    @PrimaryKey val id: String,
    @ColumnInfo(name = "customer_name") val customerName: String,
    val status: String,
    @ColumnInfo(name = "total_cents") val totalCents: Int,
    @ColumnInfo(name = "created_at") val createdAt: Long,
    @ColumnInfo(name = "updated_at") val updatedAt: Long,
    @ColumnInfo(name = "synced_at") val syncedAt: Long = System.currentTimeMillis(),
)

@Database(entities = [OrderEntity::class, OrderItemEntity::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
    abstract fun orderDao(): OrderDao
}

Hilt Module

// di/AppModule.kt
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    
    @Provides
    @Singleton
    fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder()
        .addInterceptor(AuthInterceptor())
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = if (BuildConfig.DEBUG) BODY else NONE
        })
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()
    
    @Provides
    @Singleton
    fun provideRetrofit(okHttp: OkHttpClient): Retrofit = Retrofit.Builder()
        .baseUrl("https://api.myapp.com/v1/")
        .client(okHttp)
        .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
        .build()
    
    @Provides
    @Singleton
    fun provideOrderService(retrofit: Retrofit): OrderService =
        retrofit.create(OrderService::class.java)
    
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
        Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
            .addMigrations(MIGRATION_1_2)
            .build()
    
    @Provides
    fun provideOrderDao(db: AppDatabase): OrderDao = db.orderDao()
}

For the iOS equivalent of this Android development guide, see the iOS Swift guide for SwiftUI, Swift Concurrency, and SwiftData patterns. For the cross-platform alternative with React Native, the React Native Expo guide covers sharing code between iOS and Android. The Claude Skills 360 bundle includes Android/Kotlin skill sets covering Jetpack Compose, ViewModel, and Room patterns. Start with the free tier to try Compose screen generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free