Kotlin Android Development with Jetpack Compose
Build modern Android applications using Kotlin, Jetpack Compose, and Android Architecture Components
# Kotlin Android Development with Jetpack Compose
## 1. Project Setup and Architecture
### Modern Android Project Structure
```
app/
├── src/
│ ├── main/
│ │ ├── java/com/yourapp/
│ │ │ ├── MyApplication.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── ui/
│ │ │ │ ├── theme/
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Type.kt
│ │ │ │ ├── components/
│ │ │ │ ├── screens/
│ │ │ │ │ ├── home/
│ │ │ │ │ ├── profile/
│ │ │ │ │ └── settings/
│ │ │ │ └── navigation/
│ │ │ ├── data/
│ │ │ │ ├── local/
│ │ │ │ │ ├── database/
│ │ │ │ │ └── preferences/
│ │ │ │ ├── remote/
│ │ │ │ │ ├── api/
│ │ │ │ │ └── dto/
│ │ │ │ └── repository/
│ │ │ ├── domain/
│ │ │ │ ├── model/
│ │ │ │ ├── repository/
│ │ │ │ └── usecase/
│ │ │ ├── di/
│ │ │ └── util/
│ │ ├── res/
│ │ └── AndroidManifest.xml
│ ├── test/
│ └── androidTest/
├── build.gradle.kts
└── proguard-rules.pro
```
### Gradle Configuration (Module Level)
```kotlin
// build.gradle.kts (Module: app)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
id("kotlin-parcelize")
}
android {
namespace = "com.yourapp"
compileSdk = 34
defaultConfig {
applicationId = "com.yourapp"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
// Compose BOM
implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
// Navigation
implementation("androidx.navigation:navigation-compose:2.7.5")
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
// Hilt
implementation("com.google.dagger:hilt-android:2.48")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
kapt("com.google.dagger:hilt-compiler:2.48")
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Image Loading
implementation("io.coil-kt:coil-compose:2.5.0")
// Testing
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.7.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
```
## 2. Jetpack Compose UI Components
### Reusable Composables
```kotlin
// Custom Button Component
@Composable
fun PrimaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
isLoading: Boolean = false
) {
Button(
onClick = onClick,
modifier = modifier
.fillMaxWidth()
.height(48.dp),
enabled = enabled && !isLoading,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
disabledContainerColor = MaterialTheme.colorScheme.outline,
disabledContentColor = MaterialTheme.colorScheme.onSurface
),
shape = RoundedCornerShape(12.dp)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
} else {
Text(
text = text,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
}
// Custom Text Field Component
@Composable
fun CustomTextField(
value: String,
onValueChange: (String) -> Unit,
label: String,
modifier: Modifier = Modifier,
placeholder: String = "",
isError: Boolean = false,
errorMessage: String = "",
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
visualTransformation: VisualTransformation = VisualTransformation.None,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null
) {
Column(modifier = modifier) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
label = { Text(label) },
placeholder = { Text(placeholder) },
modifier = Modifier.fillMaxWidth(),
isError = isError,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline,
errorBorderColor = MaterialTheme.colorScheme.error
)
)
if (isError && errorMessage.isNotEmpty()) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
}
}
// Custom Card Component
@Composable
fun CustomCard(
modifier: Modifier = Modifier,
elevation: Dp = 4.dp,
content: @Composable ColumnScope.() -> Unit
) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = elevation),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Column(
modifier = Modifier.padding(16.dp),
content = content
)
}
}
// Loading Indicator
@Composable
fun LoadingIndicator(
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary
)
}
}
// Error Message Component
@Composable
fun ErrorMessage(
message: String,
onRetryClick: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface
)
if (onRetryClick != null) {
Spacer(modifier = Modifier.height(16.dp))
TextButton(onClick = onRetryClick) {
Text("Retry")
}
}
}
}
```
### Custom Modifiers and Extensions
```kotlin
// Custom Modifiers
fun Modifier.shimmerEffect(): Modifier = composed {
var size by remember { mutableStateOf(IntSize.Zero) }
val transition = rememberInfiniteTransition(label = "shimmer")
val startOffsetX by transition.animateFloat(
initialValue = -2 * size.width.toFloat(),
targetValue = 2 * size.width.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(1000)
),
label = "shimmer"
)
background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFB8B5B5),
Color(0xFF8F8B8B),
Color(0xFFB8B5B5),
),
start = Offset(startOffsetX, 0f),
end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat())
)
)
.onGloballyPositioned {
size = it.size
}
}
// Clickable with ripple effect
fun Modifier.clickableRipple(
bounded: Boolean = true,
onClick: () -> Unit
): Modifier = composed {
clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = bounded),
onClick = onClick
)
}
// Conditional modifier
inline fun Modifier.conditional(
condition: Boolean,
ifTrue: Modifier.() -> Modifier,
ifFalse: (Modifier.() -> Modifier) = { this }
): Modifier = if (condition) {
then(ifTrue(Modifier))
} else {
then(ifFalse(Modifier))
}
```
## 3. MVVM Architecture with StateFlow
### Base ViewModel
```kotlin
// Base ViewModel Class
abstract class BaseViewModel : ViewModel() {
protected val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
protected val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error.asStateFlow()
protected fun handleError(throwable: Throwable) {
_isLoading.value = false
_error.value = when (throwable) {
is IOException -> "Network error. Please check your connection."
is HttpException -> "Server error: ${throwable.code()}"
else -> throwable.message ?: "An unexpected error occurred"
}
}
fun clearError() {
_error.value = null
}
protected fun launchSafe(block: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch {
try {
_isLoading.value = true
_error.value = null
block()
} catch (e: Exception) {
handleError(e)
} finally {
_isLoading.value = false
}
}
}
}
// User Profile ViewModel Example
@HiltViewModel
class UserProfileViewModel @Inject constructor(
private val userRepository: UserRepository,
private val authRepository: AuthRepository
) : BaseViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
private val _posts = MutableStateFlow<List<Post>>(emptyList())
val posts: StateFlow<List<Post>> = _posts.asStateFlow()
init {
loadUserProfile()
}
fun loadUserProfile() {
launchSafe {
val currentUser = authRepository.getCurrentUser()
_user.value = currentUser
if (currentUser != null) {
val userPosts = userRepository.getUserPosts(currentUser.id)
_posts.value = userPosts
}
}
}
fun updateProfile(name: String, bio: String) {
launchSafe {
val currentUser = _user.value ?: return@launchSafe
val updatedUser = currentUser.copy(
name = name,
bio = bio
)
val result = userRepository.updateUser(updatedUser)
_user.value = result
}
}
fun refreshPosts() {
launchSafe {
val currentUser = _user.value ?: return@launchSafe
val refreshedPosts = userRepository.getUserPosts(currentUser.id)
_posts.value = refreshedPosts
}
}
}
// UI State Pattern for Complex Screens
data class HomeUiState(
val isLoading: Boolean = false,
val posts: List<Post> = emptyList(),
val users: List<User> = emptyList(),
val error: String? = null,
val isRefreshing: Boolean = false
)
@HiltViewModel
class HomeViewModel @Inject constructor(
private val postRepository: PostRepository,
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
init {
loadData()
}
fun loadData() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
try {
val posts = async { postRepository.getPosts() }
val users = async { userRepository.getUsers() }
_uiState.value = _uiState.value.copy(
isLoading = false,
posts = posts.await(),
users = users.await()
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "An error occurred"
)
}
}
}
fun refresh() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isRefreshing = true)
try {
val posts = postRepository.getPosts()
_uiState.value = _uiState.value.copy(
isRefreshing = false,
posts = posts,
error = null
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isRefreshing = false,
error = e.message
)
}
}
}
}
```
## 4. Room Database Integration
### Database Setup
```kotlin
// Entity Classes
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: String,
val name: String,
val email: String,
val avatarUrl: String?,
val bio: String?,
@ColumnInfo(name = "created_at") val createdAt: Long,
@ColumnInfo(name = "updated_at") val updatedAt: Long
)
@Entity(
tableName = "posts",
foreignKeys = [
ForeignKey(
entity = UserEntity::class,
parentColumns = ["id"],
childColumns = ["user_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index(value = ["user_id"])]
)
data class PostEntity(
@PrimaryKey val id: String,
@ColumnInfo(name = "user_id") val userId: String,
val title: String,
val content: String,
val imageUrl: String?,
@ColumnInfo(name = "created_at") val createdAt: Long
)
// DAO Interfaces
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUserById(id: String): UserEntity?
@Query("SELECT * FROM users ORDER BY name ASC")
suspend fun getAllUsers(): List<UserEntity>
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsersFlow(): Flow<List<UserEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: UserEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(users: List<UserEntity>)
@Update
suspend fun updateUser(user: UserEntity)
@Delete
suspend fun deleteUser(user: UserEntity)
@Query("DELETE FROM users WHERE id = :id")
suspend fun deleteUserById(id: String)
}
@Dao
interface PostDao {
@Query("SELECT * FROM posts WHERE id = :id")
suspend fun getPostById(id: String): PostEntity?
@Query("SELECT * FROM posts WHERE user_id = :userId ORDER BY created_at DESC")
suspend fun getPostsByUserId(userId: String): List<PostEntity>
@Query("SELECT * FROM posts ORDER BY created_at DESC")
suspend fun getAllPosts(): List<PostEntity>
@Query("SELECT * FROM posts ORDER BY created_at DESC")
fun getAllPostsFlow(): Flow<List<PostEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPost(post: PostEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPosts(posts: List<PostEntity>)
@Update
suspend fun updatePost(post: PostEntity)
@Delete
suspend fun deletePost(post: PostEntity)
}
// Database Class
@Database(
entities = [UserEntity::class, PostEntity::class],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
}
// Type Converters
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}
```
## 5. Repository Pattern and Data Layer
### Repository Implementation
```kotlin
// Repository Interface
interface UserRepository {
suspend fun getUsers(): List<User>
suspend fun getUserById(id: String): User?
suspend fun updateUser(user: User): User
suspend fun deleteUser(id: String)
fun getUsersFlow(): Flow<List<User>>
}
// Repository Implementation
@Singleton
class UserRepositoryImpl @Inject constructor(
private val userDao: UserDao,
private val apiService: ApiService,
private val networkConnectivityManager: NetworkConnectivityManager
) : UserRepository {
override suspend fun getUsers(): List<User> {
return if (networkConnectivityManager.isConnected()) {
try {
// Fetch from API
val apiUsers = apiService.getUsers()
// Save to local database
val userEntities = apiUsers.map { it.toEntity() }
userDao.insertUsers(userEntities)
// Return domain models
apiUsers.map { it.toDomainModel() }
} catch (e: Exception) {
// Fallback to local data
userDao.getAllUsers().map { it.toDomainModel() }
}
} else {
// No network, return local data
userDao.getAllUsers().map { it.toDomainModel() }
}
}
override suspend fun getUserById(id: String): User? {
return userDao.getUserById(id)?.toDomainModel()
}
override suspend fun updateUser(user: User): User {
return if (networkConnectivityManager.isConnected()) {
try {
val apiUser = apiService.updateUser(user.toApiModel())
val userEntity = apiUser.toEntity()
userDao.updateUser(userEntity)
apiUser.toDomainModel()
} catch (e: Exception) {
throw e
}
} else {
throw NetworkException("No internet connection")
}
}
override suspend fun deleteUser(id: String) {
if (networkConnectivityManager.isConnected()) {
try {
apiService.deleteUser(id)
userDao.deleteUserById(id)
} catch (e: Exception) {
throw e
}
} else {
throw NetworkException("No internet connection")
}
}
override fun getUsersFlow(): Flow<List<User>> {
return userDao.getAllUsersFlow().map { entities ->
entities.map { it.toDomainModel() }
}
}
}
// Data Mappers
fun UserEntity.toDomainModel(): User {
return User(
id = id,
name = name,
email = email,
avatarUrl = avatarUrl,
bio = bio,
createdAt = Date(createdAt),
updatedAt = Date(updatedAt)
)
}
fun User.toEntity(): UserEntity {
return UserEntity(
id = id,
name = name,
email = email,
avatarUrl = avatarUrl,
bio = bio,
createdAt = createdAt.time,
updatedAt = updatedAt.time
)
}
fun ApiUser.toDomainModel(): User {
return User(
id = id,
name = name,
email = email,
avatarUrl = avatarUrl,
bio = bio,
createdAt = Date(createdAt * 1000), // API returns seconds
updatedAt = Date(updatedAt * 1000)
)
}
fun User.toApiModel(): ApiUser {
return ApiUser(
id = id,
name = name,
email = email,
avatarUrl = avatarUrl,
bio = bio,
createdAt = createdAt.time / 1000, // API expects seconds
updatedAt = updatedAt.time / 1000
)
}
```
## 6. Dependency Injection with Hilt
### Hilt Setup
```kotlin
// Application Class
@HiltAndroidApp
class MyApplication : Application()
// Network Module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
// Database Module
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
@Provides
fun providePostDao(database: AppDatabase): PostDao {
return database.postDao()
}
}
// Repository Module
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindUserRepository(
userRepositoryImpl: UserRepositoryImpl
): UserRepository
@Binds
abstract fun bindPostRepository(
postRepositoryImpl: PostRepositoryImpl
): PostRepository
}
```
## 7. Navigation with Jetpack Compose
### Navigation Setup
```kotlin
// Navigation Routes
sealed class Screen(val route: String) {
object Home : Screen("home")
object Profile : Screen("profile")
object Settings : Screen("settings")
object PostDetail : Screen("post_detail/{postId}") {
fun createRoute(postId: String) = "post_detail/$postId"
}
object EditProfile : Screen("edit_profile")
}
// Main Navigation Component
@Composable
fun AppNavigation(
navController: NavHostController = rememberNavController(),
startDestination: String = Screen.Home.route
) {
NavHost(
navController = navController,
startDestination = startDestination
) {
composable(Screen.Home.route) {
HomeScreen(
onNavigateToProfile = {
navController.navigate(Screen.Profile.route)
},
onNavigateToPost = { postId ->
navController.navigate(Screen.PostDetail.createRoute(postId))
}
)
}
composable(Screen.Profile.route) {
ProfileScreen(
onNavigateToEditProfile = {
navController.navigate(Screen.EditProfile.route)
},
onNavigateBack = {
navController.popBackStack()
}
)
}
composable(
route = Screen.PostDetail.route,
arguments = listOf(navArgument("postId") { type = NavType.StringType })
) { backStackEntry ->
val postId = backStackEntry.arguments?.getString("postId") ?: ""
PostDetailScreen(
postId = postId,
onNavigateBack = {
navController.popBackStack()
}
)
}
composable(Screen.EditProfile.route) {
EditProfileScreen(
onNavigateBack = {
navController.popBackStack()
},
onProfileUpdated = {
navController.popBackStack()
}
)
}
composable(Screen.Settings.route) {
SettingsScreen(
onNavigateBack = {
navController.popBackStack()
}
)
}
}
}
// Screen Examples
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
onNavigateToProfile: () -> Unit,
onNavigateToPost: (String) -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.isRefreshing,
onRefresh = viewModel::refresh
)
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
when {
uiState.isLoading -> {
LoadingIndicator()
}
uiState.error != null -> {
ErrorMessage(
message = uiState.error,
onRetryClick = viewModel::loadData
)
}
else -> {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(uiState.posts) { post ->
PostCard(
post = post,
onClick = { onNavigateToPost(post.id) }
)
}
}
}
}
PullRefreshIndicator(
refreshing = uiState.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
```
## Checklist for Kotlin Android Development
- [ ] Set up modern Android project structure with proper modularization
- [ ] Configure Gradle with latest dependencies and build optimization
- [ ] Implement MVVM architecture with StateFlow and Compose
- [ ] Create reusable Compose components and custom modifiers
- [ ] Set up Room database with proper entities and DAOs
- [ ] Implement repository pattern with offline-first approach
- [ ] Configure Hilt for dependency injection
- [ ] Set up navigation with Jetpack Compose Navigation
- [ ] Add proper error handling and loading states
- [ ] Implement data mapping between layers
- [ ] Add comprehensive unit and UI tests
- [ ] Configure ProGuard/R8 for release builds
- [ ] Implement proper network error handling and retry logic
- [ ] Add accessibility support and proper content descriptions