| name | android |
| description | Build Android applications with Kotlin, Jetpack Compose, and modern Android architecture |
Kotlin + Android
When to activate
- Building Android apps with Jetpack Compose
- Setting up ViewModel with StateFlow or SharedFlow
- Configuring Room database (entity, DAO, database class)
- Integrating Retrofit for HTTP and Moshi/Gson for JSON
- Wiring dependency injection with Hilt
- Preparing a release for Google Play Store submission
When NOT to use
- Cross-platform Flutter or React Native projects
- Android apps still using Views/XML layouts with no Compose involvement
- Kotlin Multiplatform (KMP) projects where Android is just one target
Instructions
Compose Recomposition Optimization
Recomposition reruns composable functions when their inputs change. Avoid unnecessary work:
Use remember to cache computed values and objects across recompositions:
@Composable
fun ExpensiveList(items: List<Item>) {
val sorted = items.sortedBy { it.name }
val sorted = remember(items) { items.sortedBy { it.name } }
LazyColumn {
items(sorted, key = { it.id }) { item ->
ItemRow(item)
}
}
}
Use derivedStateOf for values derived from observable state to reduce recomposition frequency:
@Composable
fun ScrollToTopButton(listState: LazyListState) {
val showButton = listState.firstVisibleItemIndex > 0
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
AnimatedVisibility(visible = showButton) {
FloatingActionButton(onClick = { }) {
Icon(Icons.Default.KeyboardArrowUp, contentDescription = "Scroll to top")
}
}
}
Stability: Compose skips recomposition of composables whose parameters have not changed — but only if all parameters are considered "stable" (primitives, @Stable/@Immutable annotated classes, or classes Compose infers as stable). Data classes with only stable fields are inferred as stable automatically.
@Composable
fun ItemList(items: List<Item>) { ... }
@Immutable
data class ItemsState(val items: ImmutableList<Item>)
@Composable
fun ItemList(state: ItemsState) { ... }
Use kotlinx.collections.immutable (ImmutableList, PersistentList) for stable collections in Compose.
ViewModel with StateFlow and SharedFlow
data class ProductUiState(
val products: List<Product> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
)
sealed interface ProductEvent {
data object NavigateToCart : ProductEvent
data class ShowSnackbar(val message: String) : ProductEvent
}
@HiltViewModel
class ProductViewModel @Inject constructor(
private val repo: ProductRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow(ProductUiState())
val uiState: StateFlow<ProductUiState> = _uiState.asStateFlow()
private val _events = MutableSharedFlow<ProductEvent>()
val events: SharedFlow<ProductEvent> = _events.asSharedFlow()
init {
loadProducts()
}
fun loadProducts() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, error = null) }
repo.getProducts()
.onSuccess { products ->
_uiState.update { it.copy(products = products, isLoading = false) }
}
.onFailure { e ->
_uiState.update { it.copy(isLoading = , error = e.message) }
}
}
}
{
viewModelScope.launch {
repo.addToCart(product)
_events.emit(ProductEvent.ShowSnackbar())
}
}
}
) {
uiState vm.uiState.collectAsStateWithLifecycle()
snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect() {
vm.events.collect { event ->
(event) {
ProductEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.message)
ProductEvent.NavigateToCart -> { }
}
}
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
}
}
Room — Entity, DAO, Database Setup
@Entity(tableName = "products")
data class ProductEntity(
@PrimaryKey val id: Int,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "price_cents") val priceCents: Int,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
)
@Dao
interface ProductDao {
@Query("SELECT * FROM products ORDER BY name ASC")
fun observeAll(): Flow<List<ProductEntity>>
@Query("SELECT * FROM products WHERE id = :id")
suspend fun findById(id: Int): ProductEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(products: List<ProductEntity>)
@Delete
suspend fun delete(product: ProductEntity)
}
: () {
: ProductDao
{
INSTANCE: AppDatabase? =
: AppDatabase =
INSTANCE ?: synchronized() {
Room.databaseBuilder(context, AppDatabase::.java, )
.addMigrations(MIGRATION_1_2)
.build()
.also { INSTANCE = it }
}
}
}
MIGRATION_1_2 = : Migration(, ) {
{
db.execSQL()
}
}
Provide via Hilt (see module section below). Always use Flow<T> in DAOs for reactive data; use suspend for one-shot queries.
Retrofit + Moshi
data class ProductResponse(
@Json(name = "id") val id: Int,
@Json(name = "name") val name: String,
@Json(name = "price") val price: Double,
)
interface ProductService {
@GET("products")
suspend fun getProducts(
@Query("category") category: String? = null,
@Query("page") page: Int = 1,
): List<ProductResponse>
@GET("products/{id}")
suspend fun getProduct(@Path("id") id: Int): ProductResponse
@POST("products")
suspend fun createProduct(@Body body: CreateProductRequest): ProductResponse
}
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/v1/")
.addConverterFactory(
MoshiConverterFactory.create(
Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
)
)
.client(
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
HttpLoggingInterceptor.Level.NONE
})
.addInterceptor { chain ->
request = chain.request().newBuilder()
.addHeader(, )
.build()
chain.proceed(request)
}
.build()
)
.build()
Hilt Module Setup
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.build()
@Provides @Singleton
fun provideMoshi(): Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
@Provides @Singleton
fun provideRetrofit(client: OkHttpClient, moshi: Moshi): Retrofit =
Retrofit.Builder()
.baseUrl("https://api.example.com/v1/")
.client(client)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
@Provides @Singleton
fun provideProductService(retrofit: Retrofit): ProductService =
retrofit.create(ProductService::class.java)
}
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides @Singleton
fun provideDatabase(@ApplicationContext ctx: Context): AppDatabase =
AppDatabase.getInstance(ctx)
@Provides
: ProductDao = db.productDao()
}
: ()
Annotate Activity, Fragment, ViewModel, Service with @AndroidEntryPoint to enable field injection. Use @HiltViewModel on ViewModels.
Play Store Listing Requirements
Before submitting a release:
Technical:
targetSdkVersion at least the current requirement (Google updates this annually)
- 64-bit support: build with ABI splits or universal APK including arm64-v8a
- App signing: enroll in Play App Signing; upload key stays with you, Google manages delivery key
- App Bundle (
.aab) required for new apps — not APK
- Permissions declared in
AndroidManifest.xml must be justified in the data safety form
Store listing assets:
- Feature graphic: 1024x500px
- Icon: 512x512px (separate from app icon; used in Play Store)
- Screenshots: at least 2 per declared device type (phone, tablet, Chromebook, TV)
- Short description: 80 characters
- Full description: up to 4000 characters
Data safety section:
- Declare all data types collected, transmitted, or shared
- Indicate whether data is linked to user identity
- Declare whether data is used for tracking
- Third-party SDKs (ads, analytics) often collect data — disclose their collection too
Pre-launch report:
- Run automated tests via Firebase Test Lab through Play Console
- Review crash reports and ANRs before promoting to production
Example
A product list screen backed by Room + Retrofit with offline-first caching:
class ProductRepository @Inject constructor(
private val service: ProductService,
private val dao: ProductDao,
) {
fun observeProducts(): Flow<List<Product>> =
dao.observeAll().map { entities -> entities.map { it.toProduct() } }
suspend fun syncProducts() {
val remote = service.getProducts()
dao.deleteAll()
dao.upsertAll(remote.map { it.toEntity() })
}
}
@Composable
fun ProductListScreen(vm: ProductViewModel = hiltViewModel()) {
val state by vm.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { vm.sync() }
LazyColumn {
items(state.products, key = { it.id }) { product ->
ProductCard(
product = product,
onAddToCart = { vm.onAddToCartClick(product) },
)
}
}
}