Toolmingo
Guides16 min read

Kotlin Developer Roadmap 2025 (Step-by-Step Guide)

The complete Kotlin developer roadmap for 2025 — language fundamentals, coroutines, Android with Jetpack Compose, Spring Boot backend, testing, and DevOps. Know exactly what to learn and in what order to land your first Kotlin job.

Kotlin is JetBrains' modern, concise language that runs on the JVM — the official language for Android development, and increasingly popular for backend services with Spring Boot and Ktor. This roadmap shows you exactly what to learn, in what order, and realistic timelines to go from zero to a job-ready Kotlin developer.

At a glance

Phase Topics Time estimate
1 Kotlin fundamentals — syntax, types, null safety 3–4 weeks
2 Object-oriented and functional programming 3–4 weeks
3 Coroutines and asynchronous programming 3–4 weeks
4 Android with Jetpack Compose 6–8 weeks
5 Architecture — MVVM, DI, Clean Architecture 3–4 weeks
6 Data layer — Room, Retrofit, DataStore 2–3 weeks
7 Testing — unit, UI, integration 2–3 weeks
8 Publishing to the Play Store 1–2 weeks
9 Backend with Ktor or Spring Boot (optional) 4–6 weeks
10 Kotlin Multiplatform (KMP) 4–6 weeks

Total: 10–14 months to job-ready (at ~2 hours/day)


Phase 1 — Kotlin fundamentals

Setup

# Install via SDKMAN (Linux/macOS)
sdk install kotlin

# Or use IntelliJ IDEA (recommended) — Kotlin is bundled
# Download Android Studio for mobile development

Variables and null safety

// Immutable (prefer this)
val name: String = "Alice"
val count = 42              // type inferred

// Mutable
var score = 0
score = 10

// Nullable types — Kotlin's killer feature
val maybeNull: String? = null
val length = maybeNull?.length ?: 0   // safe call + elvis operator
val forced = maybeNull!!.length       // throws NPE if null — avoid

Data types

val integer: Int = 42
val long: Long = 100_000L
val double: Double = 3.14
val float: Float = 3.14f
val boolean: Boolean = true
val char: Char = 'A'
val string: String = "Hello, Kotlin"
val multiline = """
    Line 1
    Line 2
""".trimIndent()

Functions

// Basic function
fun greet(name: String): String = "Hello, $name"

// Default parameters
fun connect(host: String, port: Int = 8080) = "$host:$port"

// Named arguments
connect(port = 9090, host = "localhost")

// Extension functions
fun String.capitalize(): String = 
    this.replaceFirstChar { it.uppercase() }

"hello".capitalize() // "Hello"

// Higher-order function
fun applyTwice(value: Int, f: (Int) -> Int) = f(f(value))
applyTwice(3) { it * 2 } // 12

Control flow

// When expression (powerful switch)
val result = when (score) {
    in 90..100 -> "A"
    in 80..89  -> "B"
    in 70..79  -> "C"
    else       -> "F"
}

// For loop
for (i in 1..10) println(i)
for (i in 10 downTo 1 step 2) println(i)

// Ranges as collections
val range = (1..5).toList()         // [1, 2, 3, 4, 5]
val filtered = (1..20).filter { it % 3 == 0 } // [3, 6, 9, 12, 15, 18]

Collections

// Immutable (default)
val list = listOf(1, 2, 3)
val map = mapOf("a" to 1, "b" to 2)
val set = setOf("x", "y")

// Mutable
val mutableList = mutableListOf(1, 2, 3)
mutableList.add(4)

// Collection operations
val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }         // [2, 4, 6, 8, 10]
val evens = numbers.filter { it % 2 == 0 }   // [2, 4]
val sum = numbers.reduce { acc, n -> acc + n } // 15
val grouped = numbers.groupBy { if (it % 2 == 0) "even" else "odd" }

Phase 2 — OOP and functional programming

Data classes

data class User(
    val id: Long,
    val name: String,
    val email: String,
    val role: Role = Role.USER
)

enum class Role { USER, ADMIN, MODERATOR }

// Free: equals, hashCode, toString, copy
val alice = User(1, "Alice", "alice@example.com")
val bob = alice.copy(name = "Bob", id = 2)
println(alice == bob) // false

Sealed classes — better enums

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String, val code: Int = 0) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

fun handle(result: Result<User>) = when (result) {
    is Result.Success -> println("Got user: ${result.data.name}")
    is Result.Error   -> println("Error ${result.code}: ${result.message}")
    Result.Loading    -> println("Loading…")
}

Interfaces and abstract classes

Concept Interface Abstract class
Multiple inheritance Yes No (single)
State (fields) No (default impl only) Yes
Constructors No Yes
Use when Define a contract Share implementation
interface Drawable {
    fun draw()
    fun resize(factor: Double) { /* default impl */ }
}

abstract class Shape(val color: String) : Drawable {
    abstract fun area(): Double
}

class Circle(color: String, val radius: Double) : Shape(color) {
    override fun area() = Math.PI * radius * radius
    override fun draw() = println("Drawing $color circle")
}

Object expressions and companions

// Singleton
object AppConfig {
    const val BASE_URL = "https://api.example.com"
    var debug = false
}

// Companion object (static-like)
class UserRepository {
    companion object {
        fun create(): UserRepository = UserRepository()
    }
}
val repo = UserRepository.create()

Functional programming

// Lambdas are first-class
val multiply: (Int, Int) -> Int = { a, b -> a * b }

// Function composition
fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C = { f(g(it)) }

// Inline functions for performance
inline fun <T> measure(block: () -> T): T {
    val start = System.currentTimeMillis()
    val result = block()
    println("Took ${System.currentTimeMillis() - start}ms")
    return result
}

// Sequence for lazy evaluation (like Java Streams)
val result = (1..1_000_000)
    .asSequence()
    .filter { it % 2 == 0 }
    .map { it * it }
    .take(5)
    .toList()

Phase 3 — Coroutines

Kotlin coroutines are the cornerstone of async Kotlin — lighter than threads and more structured than callbacks.

Basics

// build.gradle.kts
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        delay(1000)
        println("Coroutine done")
    }
    println("Main continues")
    job.join()
}

async / await

suspend fun fetchUser(id: Long): User { /* suspend call */ }
suspend fun fetchPosts(userId: Long): List<Post> { /* suspend call */ }

// Sequential
val user = fetchUser(1)
val posts = fetchPosts(user.id)

// Parallel
val userDeferred = async { fetchUser(1) }
val postsDeferred = async { fetchPosts(1) }
val user = userDeferred.await()
val posts = postsDeferred.await()

Coroutine scopes and contexts

Scope Use case
GlobalScope Top-level (avoid in production)
viewModelScope Android ViewModel (auto-cancelled)
lifecycleScope Android Activity/Fragment
CoroutineScope(Dispatchers.IO) Backend / general
// Dispatchers
Dispatchers.Main   // Android UI thread
Dispatchers.IO     // I/O operations, network, database
Dispatchers.Default // CPU-intensive work
Dispatchers.Unconfined // inherits caller (test use only)

// Switch context
withContext(Dispatchers.IO) {
    // runs on IO thread pool
    readFile()
}

Flow — reactive streams

import kotlinx.coroutines.flow.*

fun numberStream(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)
    }
}

// Collect
numberStream()
    .filter { it % 2 == 0 }
    .map { it * it }
    .collect { println(it) }

// StateFlow (replaces LiveData)
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

Phase 4 — Android with Jetpack Compose

Jetpack Compose is the modern declarative UI toolkit for Android — replacing XML layouts.

First composable

@Composable
fun UserCard(user: User, modifier: Modifier = Modifier) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .padding(8.dp),
        elevation = CardDefaults.cardElevation(4.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = user.name,
                style = MaterialTheme.typography.headlineSmall
            )
            Text(
                text = user.email,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

State management

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Count: $count", style = MaterialTheme.typography.headlineMedium)
        Button(onClick = { count++ }) { Text("Increment") }
    }
}

Key Compose concepts

Concept What it does
@Composable Marks a composable function
remember Survives recompositions
rememberSaveable Survives process death
State<T> Triggers recomposition on change
LaunchedEffect Side effects (coroutine) tied to composition
derivedStateOf Derived state to avoid excess recompositions
CompositionLocal Implicit data passing down the tree

Navigation

// build.gradle.kts
implementation("androidx.navigation:navigation-compose:2.8.0")

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(navController, startDestination = "home") {
        composable("home") { HomeScreen(navController) }
        composable("detail/{id}") { backStackEntry ->
            val id = backStackEntry.arguments?.getString("id")
            DetailScreen(id, navController)
        }
    }
}

Phase 5 — Architecture

MVVM + StateFlow

// ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    fun loadUser(id: Long) {
        viewModelScope.launch {
            _uiState.value = UserUiState.Loading
            _uiState.value = when (val result = repository.getUser(id)) {
                is Result.Success -> UserUiState.Success(result.data)
                is Result.Error   -> UserUiState.Error(result.message)
            }
        }
    }
}

sealed class UserUiState {
    object Loading : UserUiState()
    data class Success(val user: User) : UserUiState()
    data class Error(val message: String) : UserUiState()
}

Hilt — Dependency Injection

// Application
@HiltAndroidApp
class MyApp : Application()

// Module
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides @Singleton
    fun provideRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl(BuildConfig.BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    @Provides @Singleton
    fun provideUserApi(retrofit: Retrofit): UserApi =
        retrofit.create(UserApi::class.java)
}

// Inject
@HiltViewModel
class UserViewModel @Inject constructor(
    private val api: UserApi
) : ViewModel()

Architecture patterns

Pattern Layers Best for
MVVM Model, ViewModel, View Android standard (Google recommended)
MVI Model, View, Intent Unidirectional data flow, complex state
Clean Architecture Data, Domain, Presentation Large codebases, testability
MVC Model, View, Controller Legacy Android (avoid for new projects)

Phase 6 — Data layer

Room database

// Entity
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey val id: Long,
    @ColumnInfo(name = "display_name") val name: String,
    val email: String,
    val createdAt: Long = System.currentTimeMillis()
)

// DAO
@Dao
interface UserDao {
    @Query("SELECT * FROM users ORDER BY display_name")
    fun getAllUsers(): Flow<List<UserEntity>>

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getUserById(id: Long): UserEntity?

    @Upsert
    suspend fun upsertUser(user: UserEntity)

    @Delete
    suspend fun deleteUser(user: UserEntity)
}

// Database
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Retrofit — network calls

// API interface
interface UserApi {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Long): UserResponse

    @POST("users")
    suspend fun createUser(@Body body: CreateUserRequest): UserResponse

    @GET("users")
    suspend fun listUsers(
        @Query("page") page: Int = 1,
        @Query("limit") limit: Int = 20
    ): PagedResponse<UserResponse>
}

// Repository pattern
class UserRepository(
    private val api: UserApi,
    private val dao: UserDao
) {
    suspend fun getUser(id: Long): Result<User> = runCatching {
        api.getUser(id).toDomain()
    }.fold(
        onSuccess = { Result.Success(it) },
        onFailure = { Result.Error(it.message ?: "Unknown error") }
    )
}

DataStore (replaces SharedPreferences)

// Preferences DataStore
val Context.dataStore by preferencesDataStore("settings")

object PreferencesKeys {
    val THEME = stringPreferencesKey("theme")
    val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications")
}

// Read
val themeFlow: Flow<String> = context.dataStore.data
    .map { it[PreferencesKeys.THEME] ?: "system" }

// Write
suspend fun setTheme(theme: String) {
    context.dataStore.edit { prefs ->
        prefs[PreferencesKeys.THEME] = theme
    }
}

Phase 7 — Testing

Testing pyramid

Level Framework Scope Speed
Unit JUnit 5 + MockK Functions, ViewModels Fast
Integration JUnit 5 + Room in-memory Repository + DB Medium
UI Compose UI tests Screens Slow
E2E Espresso / Maestro Full app flows Slowest

Unit testing with MockK

@ExtendWith(MockKExtension::class)
class UserViewModelTest {
    @MockK lateinit var repository: UserRepository

    private lateinit var viewModel: UserViewModel

    @BeforeEach
    fun setup() {
        MockKAnnotations.init(this)
        viewModel = UserViewModel(repository)
    }

    @Test
    fun `loadUser emits Success state`() = runTest {
        val user = User(1, "Alice", "alice@example.com")
        coEvery { repository.getUser(1) } returns Result.Success(user)

        viewModel.loadUser(1)

        assertEquals(UserUiState.Success(user), viewModel.uiState.value)
    }

    @Test
    fun `loadUser emits Error on failure`() = runTest {
        coEvery { repository.getUser(1) } returns Result.Error("Not found")

        viewModel.loadUser(1)

        assertTrue(viewModel.uiState.value is UserUiState.Error)
    }
}

Compose UI testing

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun counter_incrementsOnButtonClick() {
    composeTestRule.setContent { Counter() }

    composeTestRule.onNodeWithText("Count: 0").assertIsDisplayed()
    composeTestRule.onNodeWithText("Increment").performClick()
    composeTestRule.onNodeWithText("Count: 1").assertIsDisplayed()
}

Phase 8 — Publishing to the Play Store

Build configuration

// build.gradle.kts (app)
android {
    defaultConfig {
        applicationId = "com.example.myapp"
        minSdk = 26
        targetSdk = 35
        versionCode = 1
        versionName = "1.0.0"
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
            signingConfig = signingConfigs.getByName("release")
        }
    }
}

Release checklist

Step Details
Keystore Generate once, store securely
Signing Sign APK/AAB with release key
ProGuard/R8 Enable code shrinking + obfuscation
App Bundle (AAB) Upload AAB, not APK (Play Store requirement)
Play Console Create app, upload AAB, fill store listing
Content rating Complete questionnaire
Privacy policy Required for most apps
Testing tracks Internal → Closed → Open → Production

GitHub Actions CI/CD

name: Android CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Run unit tests
        run: ./gradlew test
      - name: Build release AAB
        run: ./gradlew bundleRelease

Phase 9 — Backend with Ktor or Spring Boot

Ktor (Kotlin-native, lightweight)

// build.gradle.kts
implementation("io.ktor:ktor-server-netty:2.3.12")
implementation("io.ktor:ktor-server-content-negotiation:2.3.12")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")

// Application
fun main() {
    embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}

fun Application.module() {
    install(ContentNegotiation) { json() }

    routing {
        route("/api/users") {
            get { call.respond(userService.getAllUsers()) }
            get("/{id}") {
                val id = call.parameters["id"]?.toLong()
                    ?: return@get call.respond(HttpStatusCode.BadRequest)
                call.respond(userService.getUser(id))
            }
            post {
                val request = call.receive<CreateUserRequest>()
                call.respond(HttpStatusCode.Created, userService.createUser(request))
            }
        }
    }
}

Spring Boot with Kotlin

// @RestController with Kotlin data classes
@RestController
@RequestMapping("/api/users")
class UserController(private val service: UserService) {

    @GetMapping
    fun list(): List<UserDto> = service.findAll()

    @GetMapping("/{id}")
    fun get(@PathVariable id: Long): UserDto = service.findById(id)

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun create(@RequestBody @Valid request: CreateUserRequest): UserDto =
        service.create(request)
}

// Entity with Kotlin data class
@Entity @Table(name = "users")
data class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    @Column(nullable = false) val name: String,
    @Column(unique = true) val email: String
)

Ktor vs Spring Boot

Ktor Spring Boot
Language Kotlin-first Java + Kotlin support
Size Lightweight, modular Full-featured, opinionated
Performance Excellent (coroutines) Good (WebFlux for async)
Learning curve Lower Higher
Ecosystem Growing Mature, enterprise
Best for Microservices, APIs Enterprise, existing Java teams

Phase 10 — Kotlin Multiplatform (KMP)

KMP lets you share business logic between Android, iOS, backend, and web from a single codebase.

// Shared module — commonMain
expect fun currentPlatform(): String

class UserRepository {
    suspend fun getUser(id: Long): User {
        // shared networking logic
        return api.fetchUser(id)
    }
}

// androidMain
actual fun currentPlatform(): String = "Android ${Build.VERSION.SDK_INT}"

// iosMain
actual fun currentPlatform(): String = "iOS ${UIDevice.currentDevice.systemVersion}"

KMP stack

Layer Library
Networking Ktor Client (KMP)
Serialization kotlinx.serialization
SQL SQLDelight
DI Koin (KMP)
UI (Android) Jetpack Compose
UI (iOS) SwiftUI + Compose Multiplatform
Async kotlinx.coroutines

Full technology map

Kotlin Developer
├── Language
│   ├── Null safety, data classes, sealed classes
│   ├── Extension functions, lambdas
│   └── Coroutines + Flow
│
├── Android (Mobile Track)
│   ├── Jetpack Compose (UI)
│   ├── ViewModel + StateFlow (MVVM)
│   ├── Hilt (DI)
│   ├── Room (local DB)
│   ├── Retrofit (networking)
│   └── Navigation Compose
│
├── Backend (Server Track)
│   ├── Ktor (Kotlin-native)
│   └── Spring Boot (enterprise)
│
├── Cross-platform
│   └── Kotlin Multiplatform (KMP)
│
├── Testing
│   ├── JUnit 5
│   ├── MockK
│   └── Compose UI tests
│
└── DevOps
    ├── Gradle (Kotlin DSL)
    ├── GitHub Actions
    └── Google Play Console

Realistic timeline (at ~2 hours/day)

Month Focus
1 Kotlin fundamentals — syntax, null safety, functions
2 OOP, data classes, sealed classes, collections
3 Coroutines, Flow, async programming
4–5 Jetpack Compose UI + Navigation
6–7 MVVM, Hilt DI, Room, Retrofit
8 Testing — MockK, Compose UI tests
9 Publish first Play Store app
10–14 KMP or backend (Ktor/Spring Boot) + portfolio projects

Portfolio projects

Project Skills demonstrated
To-Do app Compose, Room, ViewModel, StateFlow
News reader Retrofit, Paging 3, Coil image loading
Weather app Location API, network calls, offline caching
Chat app WebSockets / Firebase Realtime DB, auth
Finance tracker Charts (MPAndroidChart), biometric auth, DataStore
KMP app Shared logic on Android + iOS

Kotlin developer roles and salary

Role US salary (median) EU salary Notes
Junior Android Developer $80–110k €40–60k Kotlin + Compose basics
Mid Android Developer $110–150k €60–85k 2–4 years, architecture
Senior Android Developer $150–200k €80–110k System design, mentoring
Staff / Lead Android $200–260k €100–140k Team strategy
Kotlin Backend Developer $120–170k €60–95k Ktor / Spring Boot
KMP Engineer $140–190k €75–110k Android + iOS shared logic

Note: Kotlin + Compose is one of the highest-paying mobile stacks in 2025. Android developers with KMP experience command a 15–25% premium.


Common mistakes

Mistake Why it's a problem Fix
Overusing !! Crashes at runtime on null Use ?. safe call or ?: default
var instead of val Mutable state causes bugs Default to val, use var only when needed
Blocking coroutines with Thread.sleep Blocks the thread Use delay() inside coroutines
Launching coroutines in GlobalScope Memory leaks, not cancellable Use viewModelScope / lifecycleScope
LiveData in new projects Deprecated path Use StateFlow + collectAsStateWithLifecycle
Missing @Stable on Compose params Unnecessary recompositions Annotate or use @Immutable data classes
Not using sealed class for state Incomplete when branches Model UI state with sealed classes
Loading images without caching Slow scroll, wasted data Use Coil with memory + disk cache

Kotlin vs related languages

Kotlin Java Swift Dart (Flutter)
Primary use Android, JVM backend JVM backend, Android iOS, macOS Cross-platform mobile
Null safety Built-in (?) Optional (annotations) Built-in (?) Built-in (?)
Coroutines Yes (structured) Project Loom (Java 21+) async/await async/await
Android support First-class (official) Legacy No Via Flutter
iOS support KMP (shared logic) No First-class Via Flutter
Syntax verbosity Concise Verbose Concise Concise
Interop 100% Java interop N/A Obj-C interop N/A
Learning curve Low (if Java known) Medium Medium Low

FAQ

Do I need to learn Java before Kotlin? No. Kotlin is a better entry point for Android development. You'll encounter Java in legacy codebases and can learn it alongside Kotlin, but starting with Kotlin is the modern approach.

Is Kotlin only for Android? No — Kotlin runs on the JVM, so it works for backend development (Spring Boot, Ktor), data science (Jupyter), command-line tools, and with KMP you can target iOS, web, and desktop.

Kotlin vs Flutter for cross-platform? Both are good choices. Kotlin Multiplatform shares business logic while keeping native UIs (Compose for Android, SwiftUI for iOS). Flutter shares the entire UI layer via its own rendering engine. KMP gives more native look-and-feel; Flutter gives a single codebase for UI.

How long does it take to get a Kotlin job? With daily 2-hour practice: 8–12 months to junior-level Android developer. Having Java experience cuts this to 4–6 months.

Is Jetpack Compose stable for production? Yes — Compose reached stable in 2021 and is now the default UI approach for new Android apps. Google's own apps (Maps, Gmail, YouTube) use Compose.

What IDE should I use? Android Studio (based on IntelliJ) for mobile development. IntelliJ IDEA Community or Ultimate for backend Kotlin. Both have first-class Kotlin support maintained by JetBrains.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools