Building Your First Android App with Kotlin
Kotlin is Google’s preferred language for Android development—it is concise, null-safe, and fully interoperable with Java. Modern Android development uses Jetpack Compose for UI, the Navigation component for screen routing, and ViewModel + Room for data management. This article walks through building a simple note-taking app from scratch, covering the essential components every Android app needs.
Project Setup and Dependencies
Android Studio creates new projects with Gradle as the build system. The build.gradle.kts file declares dependencies and configuration. A modern Android project includes Jetpack Compose (UI framework), Navigation Compose (screen routing), and optional libraries like Room (local database) and Retrofit (networking). The minimum SDK version determines which Android versions your app supports—API 24 (Android 7.0) covers over 95% of active devices. Using version catalogs (libs.versions.toml) keeps dependency versions organized.
// build.gradle.kts (app module)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
namespace = "com.example.notepad"
compileSdk = 35
defaultConfig {
applicationId = "com.example.notepad"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
}
dependencies {
implementation(platform("androidx.compose:compose-bom:2025.01.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.navigation:navigation-compose:2.8.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
}
Building the UI with Compose
Jetpack Compose uses composable functions to declare UI. Unlike the old View system with XML layouts, Compose is declarative—you describe what the UI should look like for a given state, and Compose handles updating the screen when state changes. A composable function annotated with @Composable takes optional parameters and emits UI elements. Material 3 provides Modern design components: Scaffold (screen structure), TopAppBar, FloatingActionButton, and themed surfaces.
@Composable
fun NoteListScreen(
notes: List<Note>,
onAddNote: () -> Unit,
onNoteClick: (Int) -> Unit
) {
Scaffold(
topBar = { TopAppBar(title = { Text("My Notes") }) },
floatingActionButton = {
FloatingActionButton(onClick = onAddNote) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
}
) { padding ->
LazyColumn(modifier = Modifier.padding(padding)) {
items(notes, key = { it.id }) { note ->
NoteCard(note = note, onClick = { onNoteClick(note.id) })
}
}
}
}
@Composable
fun NoteCard(note: Note, onClick: () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth().padding(8.dp).clickable(onClick = onClick),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(note.title, style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(4.dp))
Text(note.content, maxLines = 2, style = MaterialTheme.typography.bodyMedium)
}
}
}
ViewModel and State Management
ViewModel holds UI state and survives configuration changes (screen rotation). It exposes state as StateFlow or Compose MutableState, and the UI observes this state to recompose when data changes. Never store state directly in composables—they are recreated frequently. The ViewModel uses viewModelScope for coroutines and integrates with Room via Repository pattern. Navigation between screens uses NavHost with a route string, and data is passed via savedStateHandle or shared ViewModel.
class NoteViewModel(private val dao: NoteDao) : ViewModel() {
private val _notes = MutableStateFlow<List<Note>>(emptyList())
val notes: StateFlow<List<Note>> = _notes.asStateFlow()
init {
viewModelScope.launch {
dao.getAllNotes().collect { _notes.value = it }
}
}
fun addNote(title: String, content: String) {
viewModelScope.launch {
dao.insert(Note(title = title, content = content))
}
}
}
Testing Android apps uses Compose UI testing (createComposeRule) for UI tests and JUnit + MockK for ViewModel tests. Room databases can be tested with an in-memory instance. Modern Android development emphasizes clean architecture: UI layer (Compose), domain layer (use cases), and data layer (Room + Retrofit), with dependency injection via Hilt or Koin to wire them together.
Dependency Injection with Hilt
Hilt is Google’s dependency injection library for Android, built on Dagger. It provides a standard way to provide dependencies (ViewModels, Repositories, database instances) throughout the app without manual construction. Hilt annotations (@HiltAndroidApp, @AndroidEntryPoint, @Inject, @Module, @Provides) reduce boilerplate compared to manual DI. The ViewModel is provided through @HiltViewModel and injected into composables with hiltViewModel(). Hilt modules define how to create dependencies like Room databases or Retrofit API clients, with scoping (@Singleton, @ViewModelScoped, @FragmentScoped) controlling their lifecycle. Using Hilt makes the code more testable—you can replace real dependencies with mocks in tests by providing a test module.
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides @Singleton
fun provideDatabase(@ApplicationContext ctx: Context): AppDatabase {
return Room.databaseBuilder(ctx, AppDatabase::class.java, "app.db").build()
}
@Provides fun provideNoteDao(db: AppDatabase) = db.noteDao()
}
Publishing to Google Play
Releasing an Android app on Google Play involves: creating a developer account ($25 one-time fee), preparing a signed release bundle (AAB format), creating a store listing, and defining the testing track (internal testing, closed alpha, open beta, production). Google Play’s Managed Publishing enables phased rollouts for monitoring crash rates before full release. The Play Console provides crash reports, performance data, and user ratings with sentiment analysis. For pre-release testing, Firebase Test Lab runs automated tests on a range of physical devices across API levels 24-35. Android App Bundles reduce download size by generating APKs specific to each device configuration, typically saving 30-50% download size.
