Kotlin Android Tutorial: Learn Modern Android from Scratch (2026)
Kotlin changed how I think about Android development. After years of Java with its verbosity and null pointer exceptions, Kotlin felt like a breath of fresh air — null safety, extension functions, coroutines, and data classes eliminated entire categories of bugs from my projects. When Google announced first-class Kotlin support in 2017 and Jetpack Compose in 2021, I knew the Android ecosystem had reached a turning point. This tutorial focuses on the modern Kotlin-first Android stack: Jetpack Compose for UI, coroutines for async, and Architecture Components for lifecycle-aware data management.
Kotlin Language Fundamentals for Android
Kotlin is fully interoperable with Java but far more expressive. Start with the type system — everything is an object, nullable types are marked with ?, and the compiler enforces null checks. Extension functions let you add methods to existing classes without inheritance: fun Context.showToast(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(). Data classes automatically generate equals, hashCode, toString, and copy. Sealed classes model restricted hierarchies like network states — success, loading, error. And coroutines replace callbacks and AsyncTask for async operations with structured concurrency.
data class User(val id: Int, val name: String)
sealed class Result { data class Success(val data: T) : Result()
data class Error(val message: String) : Result() }
Jetpack Compose: Declarative UI
Jetpack Compose is Android's modern UI toolkit built with Kotlin. Instead of XML layouts, you define composable functions annotated with @Composable. These functions describe the UI in response to state. A Text, Button, or Column is just a function call. Compose re-composes only the parts of the UI that change when state updates. Key modifiers like .padding(), .fillMaxWidth(), and .clickable { } chain together to configure widgets. State is managed with mutableStateOf, remember (scoped to composition), and StateFlow (scoped to ViewModel).
@Composable
fun Greeting(name: String) {
var count by remember { mutableStateOf(0) }
Column { Text("Hello $name"); Button(onClick = { count++ }) { Text("$count") } } }
ViewModel, LiveData, and StateFlow
ViewModel holds UI-related data across configuration changes like screen rotations. It outlives the Activity or Fragment lifecycle. Inside a ViewModel, you expose state via StateFlow or MutableStateFlow. In Compose, collectAsState() converts a Flow into Compose state. LiveData is the older pattern and still widely used, but StateFlow is preferred because it is Kotlin-native and works better with coroutines. Always use viewModelScope for launching coroutines inside a ViewModel — it automatically cancels them when the ViewModel is cleared.
class MyViewModel : ViewModel() {
private val _state = MutableStateFlow("initial")
val state: StateFlow = _state.asStateFlow()
fun update(val: String) { viewModelScope.launch { _state.emit(val) } } }
Navigation with the Navigation Component
The Navigation Component manages screen transitions with a type-safe Kotlin DSL. Define your nav graph in code using NavHost and composable routes. Arguments can be required or optional and are defined inline. Deep linking, bottom navigation, and conditional navigation (like login flows) are handled declaratively. The navController manages the back stack and supports saving and restoring state. Pass complex arguments using the navArgs delegate or the SavedStateHandle in ViewModels.
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("detail/{id}", arguments = listOf(navArgument("id") { type = NavType.IntType })) { DetailScreen() } }
Networking with Retrofit and Kotlin Serialization
Retrofit remains the standard HTTP client for Android. Define an interface with endpoint methods annotated with @GET, @POST, etc. Kotlin Serialization (kotlinx.serialization) replaces Gson as the JSON parser — it is faster and Kotlin-native. Combine Retrofit with coroutines by making suspend functions in your API interface. Error handling uses sealed classes: wrap each API call in a try-catch and map the result to a Success or Error sealed class instance. Use a repository pattern to abstract data sources and provide a clean API to the ViewModel.
interface ApiService {
@GET("users")
suspend fun getUsers(): List
}
val retrofit = Retrofit.Builder().baseUrl("https://api.example.com").addConverterFactory(jsonConverterFactory).build()
Material 3 Design and Theming
Material 3 (Material You) is Google's latest design system, and Compose has first-class support. Define a color scheme with lightColorScheme and darkColorScheme, and pass it to MaterialTheme. Typography uses the Typography class with TextStyle objects. Dynamic color on Android 12+ extracts colors from the user's wallpaper. Components like NavigationBar, TopAppBar, Card, and FloatingActionButton automatically adapt to the theme. The Surface composable provides the background container with the correct elevation shadows.
MaterialTheme(
colorScheme = lightColorScheme(primary = Color(0xFF6750A4), secondary = Color(0xFF625B71)),
typography = Typography(bodyLarge = TextStyle(fontFamily = FontFamily.SansSerif)),
content = { MyApp() } )
Frequently Asked Questions
Do I need to learn Java before Kotlin?
No. Kotlin is designed to be approachable on its own. If you know any programming language, you can learn Kotlin directly. Java knowledge only helps if you need to work with legacy codebases or Java-only libraries.
What is the difference between Jetpack Compose and XML layouts?
XML layouts are imperative — you describe the UI in a static file and reference it from code. Compose is declarative — you describe the UI as a function of state, and the framework handles updating it. Compose reduces boilerplate and makes dynamic UIs much easier.
When should I use a Flow vs LiveData?
Prefer Flow (StateFlow/SharedFlow) for new projects. Flow is Kotlin-native, supports all coroutine operators, and integrates seamlessly with Compose. LiveData is simpler for basic cases but does not handle errors or complex transformations as cleanly.
How do I test ViewModels and composables?
Use JUnit5 with Turbine for testing Flows and StateFlows. For Compose UI tests, use the compose-test library with SemanticsMatcher and run tests on an emulator or device with ComposeTestRule.
Originally published on Ayodhyyya. Last updated June 1, 2026.