- Published on
Kotlin Complete Guide 2025: From Spring Boot Backend to Android, Multiplatform & Coroutines
- Authors

- Name
- Youngju Kim
- @fjvbn20031
Table of Contents
1. Kotlin in 2025
Kotlin is one of the fastest-growing languages in the JVM ecosystem in 2025. Since Google adopted it as the official language for Android development, it has expanded into server-side, multiplatform, and even data science.
Kotlin Adoption Status
| Domain | Status | Primary Use |
|---|---|---|
| Android | Official primary language | 95%+ new projects in Kotlin |
| Spring Boot | Official support (1st class) | Coroutines + WebFlux integration |
| Ktor | Kotlin-native server | Lightweight microservices |
| KMP | Stable (1.9.20+) | Shared iOS/Android business logic |
| Compose Multiplatform | Beta to Stable | Shared Android/Desktop/Web UI |
| Data Science | Growing | Kotlin DataFrame, KotlinDL |
Why Kotlin
- Conciseness: 40% less code compared to Java
- Safety: Null safety at the type system level
- Interoperability: 100% compatible with existing Java code
- Coroutines: Simplified async processing with structured concurrency
- Multiplatform: Support multiple platforms from a single codebase
2. Kotlin vs Java: Comparison and Migration
Syntax Comparison
// Java
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
// Kotlin — same functionality in one line
data class User(val name: String, val age: Int)
Key Differences
| Feature | Java | Kotlin |
|---|---|---|
| Null Safety | Optional, annotations | Built into type system (?/!!) |
| Data Classes | Record (Java 16+) | data class (since 1.0) |
| Extension Functions | Not possible | Native support |
| Coroutines | Project Loom (Virtual Threads) | suspend/launch/async |
| Sealed Types | sealed (Java 17+) | sealed class/interface |
| Smart Casts | instanceof + casting | Automatic casting |
| Default Parameters | Overloading required | Default values supported |
| String Templates | Java 21+ STR | Built-in support |
| Scope Functions | None | let, run, apply, also, with |
Migrating from Java to Kotlin
// Step 1: Add Kotlin to build.gradle.kts
plugins {
kotlin("jvm") version "2.0.0"
kotlin("plugin.spring") version "2.0.0"
}
// Step 2: Convert Java files to Kotlin (IntelliJ: Ctrl+Alt+Shift+K)
// Step 3: Apply Kotlin idioms
// Step 4: Introduce coroutines
Migration strategy:
- Write new files in Kotlin
- Convert tests to Kotlin first
- Convert data classes as priority
- Gradually convert the service layer
- Introduce coroutines as the final step
3. Core Kotlin Language Features
Null Safety
// Nullable type declaration
var name: String? = null
// Safe call operator
val length = name?.length // returns null if name is null
// Elvis operator
val length = name?.length ?: 0 // default value if null
// Smart cast
if (name != null) {
println(name.length) // automatically cast to String
}
// let scope function
name?.let { nonNullName ->
println(nonNullName.length)
}
Data Class
data class User(
val id: Long,
val name: String,
val email: String,
val role: Role = Role.USER // default value
) {
// Modify immutable object with copy
fun promote() = copy(role = Role.ADMIN)
}
// Destructuring declaration
val (id, name, email) = user
Sealed Class / Sealed Interface
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable) : Result<Nothing>
data object Loading : Result<Nothing>
}
// Exhaustive when expression
fun handleResult(result: Result<User>) = when (result) {
is Result.Success -> println("User: ${result.data}")
is Result.Error -> println("Error: ${result.exception.message}")
is Result.Loading -> println("Loading...")
// no else needed — all cases handled
}
Extension Functions
// Add new method to String
fun String.toSlug(): String =
lowercase()
.replace(Regex("[^a-z0-9\\s-]"), "")
.replace(Regex("\\s+"), "-")
.trim('-')
// Usage
val slug = "Hello World 2025!".toSlug() // "hello-world-2025"
// Collection extension
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
// Generic extension
inline fun <reified T> Any.castOrNull(): T? = this as? T
Delegation
// Class delegation
interface Repository {
fun findAll(): List<User>
}
class CachedRepository(
private val delegate: Repository
) : Repository by delegate {
private val cache = mutableMapOf<String, List<User>>()
override fun findAll(): List<User> =
cache.getOrPut("all") { delegate.findAll() }
}
// Property delegation
class UserPreferences {
var theme: String by Delegates.observable("light") { _, old, new ->
println("Theme changed: $old -> $new")
}
val expensiveValue: String by lazy {
computeExpensiveValue()
}
}
DSL (Domain Specific Language)
// HTML DSL example
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
class HTML {
fun body(init: Body.() -> Unit) { /* ... */ }
}
class Body {
fun p(text: String) { /* ... */ }
fun div(init: Div.() -> Unit) { /* ... */ }
}
// Usage
val page = html {
body {
p("Hello, Kotlin DSL!")
div {
p("Nested content")
}
}
}
// Real-world: Gradle Kotlin DSL
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
}
4. Coroutines Deep Dive
Core Concepts
Coroutines are an asynchronous processing mechanism that behaves like lightweight threads. They can suspend and resume without blocking actual OS threads.
import kotlinx.coroutines.*
fun main() = runBlocking {
// launch: coroutine that doesn't return a result
val job = launch {
delay(1000L)
println("World!")
}
// async: coroutine that returns a result
val deferred = async {
delay(1000L)
42
}
println("Hello,")
job.join()
println("Answer: ${deferred.await()}")
}
suspend Functions
// suspend functions can only be called from coroutines
suspend fun fetchUser(id: Long): User {
return withContext(Dispatchers.IO) {
// Network/DB operations
userRepository.findById(id)
}
}
suspend fun fetchUserWithPosts(id: Long): UserWithPosts {
return coroutineScope {
// Parallel execution
val userDeferred = async { fetchUser(id) }
val postsDeferred = async { fetchPosts(id) }
UserWithPosts(
user = userDeferred.await(),
posts = postsDeferred.await()
)
}
}
Structured Concurrency
suspend fun processOrder(orderId: Long) = coroutineScope {
// Parent returns only when all children complete
val inventory = async { checkInventory(orderId) }
val payment = async { processPayment(orderId) }
// If one fails, the other is cancelled too
try {
val inventoryResult = inventory.await()
val paymentResult = payment.await()
completeOrder(orderId, inventoryResult, paymentResult)
} catch (e: Exception) {
cancelOrder(orderId)
throw e
}
}
Flow (Reactive Streams)
import kotlinx.coroutines.flow.*
// Cold Flow — executes on subscription
fun numberFlow(): Flow<Int> = flow {
for (i in 1..10) {
delay(100)
emit(i)
}
}
// Operator chaining
suspend fun processNumbers() {
numberFlow()
.filter { it % 2 == 0 }
.map { it * it }
.catch { e -> println("Error: $e") }
.collect { value -> println(value) }
}
// StateFlow (UI state management)
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser(id: Long) {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = repository.fetchUser(id)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
}
// SharedFlow (event broadcast)
class EventBus {
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun emit(event: Event) {
_events.emit(event)
}
}
Cancellation and Timeouts
// Coroutine cancellation
val job = launch {
repeat(1000) { i ->
println("Processing $i")
delay(500) // cancellation point
}
}
delay(2500)
job.cancelAndJoin() // cancel and wait for completion
// Timeout
val result = withTimeoutOrNull(3000L) {
fetchDataFromNetwork()
} ?: defaultValue
// Non-cancellable work
suspend fun saveToDb(data: Data) {
withContext(NonCancellable) {
// Must execute even when cancelled
database.save(data)
}
}
5. Spring Boot + Kotlin
Project Setup
// build.gradle.kts
plugins {
id("org.springframework.boot") version "3.3.0"
id("io.spring.dependency-management") version "1.1.5"
kotlin("jvm") version "2.0.0"
kotlin("plugin.spring") version "2.0.0"
kotlin("plugin.jpa") version "2.0.0"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.3")
testImplementation("io.mockk:mockk:1.13.10")
}
WebFlux + Coroutines
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {
// suspend function for async endpoint
@GetMapping("/{id}")
suspend fun getUser(@PathVariable id: Long): ResponseEntity<UserResponse> {
val user = userService.findById(id)
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(user.toResponse())
}
// Flow for streaming response
@GetMapping(produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun getAllUsers(): Flow<UserResponse> =
userService.findAll().map { it.toResponse() }
@PostMapping
suspend fun createUser(
@Valid @RequestBody request: CreateUserRequest
): ResponseEntity<UserResponse> {
val user = userService.create(request)
return ResponseEntity
.created(URI.create("/api/users/${user.id}"))
.body(user.toResponse())
}
}
@Service
class UserService(private val userRepository: UserRepository) {
suspend fun findById(id: Long): User? =
userRepository.findById(id)
fun findAll(): Flow<User> =
userRepository.findAll().asFlow()
@Transactional
suspend fun create(request: CreateUserRequest): User {
val user = User(
name = request.name,
email = request.email
)
return userRepository.save(user)
}
}
Spring Data R2DBC
interface UserRepository : CoroutineCrudRepository<User, Long> {
suspend fun findByEmail(email: String): User?
@Query("SELECT * FROM users WHERE name LIKE :pattern")
fun searchByName(pattern: String): Flow<User>
@Query("SELECT COUNT(*) FROM users WHERE active = true")
suspend fun countActive(): Long
}
@Table("users")
data class User(
@Id val id: Long? = null,
val name: String,
val email: String,
val active: Boolean = true,
val createdAt: LocalDateTime = LocalDateTime.now()
)
Testing: MockK + Kotest
class UserServiceTest : FunSpec({
val userRepository = mockk<UserRepository>()
val userService = UserService(userRepository)
test("findById should return user when exists") {
// Given
val user = User(id = 1, name = "Kim", email = "kim@test.com")
coEvery { userRepository.findById(1L) } returns user
// When
val result = userService.findById(1L)
// Then
result shouldNotBe null
result!!.name shouldBe "Kim"
coVerify(exactly = 1) { userRepository.findById(1L) }
}
test("findById should return null when not exists") {
coEvery { userRepository.findById(any()) } returns null
val result = userService.findById(999L)
result shouldBe null
}
test("create should save and return user") {
val request = CreateUserRequest("Park", "park@test.com")
val savedUser = User(id = 1, name = "Park", email = "park@test.com")
coEvery { userRepository.save(any()) } returns savedUser
val result = userService.create(request)
result.id shouldBe 1
result.name shouldBe "Park"
}
})
6. Ktor: Kotlin-Native Server
Project Configuration
// build.gradle.kts
plugins {
kotlin("jvm") version "2.0.0"
kotlin("plugin.serialization") version "2.0.0"
id("io.ktor.plugin") version "2.3.11"
}
dependencies {
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("io.ktor:ktor-server-auth-jvm")
implementation("io.ktor:ktor-server-auth-jwt-jvm")
implementation("io.ktor:ktor-server-websockets-jvm")
implementation("io.ktor:ktor-server-status-pages-jvm")
}
Routing and Plugins
fun main() {
embeddedServer(Netty, port = 8080) {
configureSerialization()
configureRouting()
configureAuth()
}.start(wait = true)
}
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
fun Application.configureRouting() {
routing {
route("/api") {
userRoutes()
postRoutes()
}
}
}
fun Route.userRoutes() {
val userService = UserService()
route("/users") {
get {
val users = userService.findAll()
call.respond(users)
}
get("/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
?: throw BadRequestException("Invalid ID")
val user = userService.findById(id)
?: throw NotFoundException("User not found")
call.respond(user)
}
post {
val request = call.receive<CreateUserRequest>()
val user = userService.create(request)
call.respond(HttpStatusCode.Created, user)
}
}
}
WebSocket
fun Application.configureWebSocket() {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(60)
maxFrameSize = Long.MAX_VALUE
}
routing {
val connections = Collections.synchronizedSet<Connection>(mutableSetOf())
webSocket("/chat") {
val connection = Connection(this)
connections += connection
try {
for (frame in incoming) {
frame as? Frame.Text ?: continue
val text = frame.readText()
connections.forEach {
it.session.send("${connection.name}: $text")
}
}
} finally {
connections -= connection
}
}
}
}
7. Android + Jetpack Compose
Compose Basics
@Composable
fun UserListScreen(
viewModel: UserListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(title = { Text("Users") })
}
) { padding ->
when (val state = uiState) {
is UiState.Loading -> LoadingIndicator(Modifier.padding(padding))
is UiState.Success -> UserList(
users = state.data,
modifier = Modifier.padding(padding)
)
is UiState.Error -> ErrorMessage(
message = state.message,
onRetry = viewModel::retry,
modifier = Modifier.padding(padding)
)
}
}
}
@Composable
fun UserList(users: List<User>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(users, key = { it.id }) { user ->
UserCard(user = user)
}
}
}
@Composable
fun UserCard(user: User) {
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.titleMedium
)
Text(
text = user.email,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
State Management and Navigation
// ViewModel
@HiltViewModel
class UserListViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<User>>> = _uiState.asStateFlow()
init { loadUsers() }
private fun loadUsers() {
viewModelScope.launch {
_uiState.value = UiState.Loading
userRepository.getUsers()
.onSuccess { users ->
_uiState.value = UiState.Success(users)
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message ?: "Unknown error")
}
}
}
fun retry() = loadUsers()
}
// Navigation (Type-safe)
@Serializable
sealed class Screen {
@Serializable
data object UserList : Screen()
@Serializable
data class UserDetail(val userId: Long) : Screen()
}
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.UserList) {
composable<Screen.UserList> {
UserListScreen(
onUserClick = { userId ->
navController.navigate(Screen.UserDetail(userId))
}
)
}
composable<Screen.UserDetail> { backStackEntry ->
val screen: Screen.UserDetail = backStackEntry.toRoute()
UserDetailScreen(userId = screen.userId)
}
}
}
8. Kotlin Multiplatform (KMP)
Project Structure
shared/
src/
commonMain/ <- Shared code
androidMain/ <- Android implementation
iosMain/ <- iOS implementation
androidApp/ <- Android app
iosApp/ <- iOS app (Swift)
expect / actual Pattern
// commonMain — define common interface
expect class PlatformContext
expect fun getPlatformName(): String
expect class DatabaseDriverFactory {
fun createDriver(): SqlDriver
}
// androidMain — Android implementation
actual typealias PlatformContext = Context
actual fun getPlatformName(): String = "Android ${Build.VERSION.SDK_INT}"
actual class DatabaseDriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver =
AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}
// iosMain — iOS implementation
actual class PlatformContext
actual fun getPlatformName(): String = UIDevice.currentDevice.systemName()
actual class DatabaseDriverFactory {
actual fun createDriver(): SqlDriver =
NativeSqliteDriver(AppDatabase.Schema, "app.db")
}
Shared Business Logic
// commonMain
class UserRepository(
private val api: UserApi,
private val db: UserDatabase,
private val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun getUsers(forceRefresh: Boolean = false): List<User> =
withContext(dispatcher) {
if (!forceRefresh) {
val cached = db.getUsers()
if (cached.isNotEmpty()) return@withContext cached
}
val users = api.fetchUsers()
db.saveUsers(users)
users
}
fun observeUsers(): Flow<List<User>> =
db.observeUsers()
}
// Ktor-based shared HTTP client
class UserApi(private val client: HttpClient) {
suspend fun fetchUsers(): List<User> =
client.get("https://api.example.com/users").body()
}
// Shared HttpClient configuration
expect fun httpClient(): HttpClient
// androidMain
actual fun httpClient(): HttpClient = HttpClient(OkHttp) {
install(ContentNegotiation) { json() }
}
// iosMain
actual fun httpClient(): HttpClient = HttpClient(Darwin) {
install(ContentNegotiation) { json() }
}
Compose Multiplatform
// commonMain — shared UI
@Composable
fun App() {
MaterialTheme {
var users by remember { mutableStateOf<List<User>>(emptyList()) }
LaunchedEffect(Unit) {
users = repository.getUsers()
}
UserListScreen(users)
}
}
// Android
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() }
}
}
// iOS (SwiftUI)
// ContentView.swift
// struct ContentView: View {
// var body: some View {
// ComposeView().ignoresSafeArea()
// }
// }
9. Advanced Features
Inline / Reified
// inline function: eliminates lambda call overhead
inline fun <T> measureTime(block: () -> T): Pair<T, Long> {
val start = System.nanoTime()
val result = block()
val duration = System.nanoTime() - start
return result to duration
}
// reified: preserves type information at runtime
inline fun <reified T> String.parseJson(): T =
Json.decodeFromString<T>(this)
// Usage
val user = """{"name":"Kim","age":30}""".parseJson<User>()
// crossinline: prohibits non-local returns
inline fun runSafe(crossinline block: () -> Unit) {
try { block() } catch (e: Exception) { /* handle */ }
}
Contracts
import kotlin.contracts.*
// Contracts tell the compiler about function behavior
fun String?.isNotNullOrEmpty(): Boolean {
contract {
returns(true) implies (this@isNotNullOrEmpty != null)
}
return this != null && this.isNotEmpty()
}
// Usage — enables smart cast
fun greet(name: String?) {
if (name.isNotNullOrEmpty()) {
println("Hello, $name!") // name smart-cast to String
}
}
Context Receivers (Experimental)
context(LoggerContext, TransactionContext)
fun transferMoney(from: Account, to: Account, amount: BigDecimal) {
log("Transfer $amount from ${from.id} to ${to.id}")
transaction {
from.withdraw(amount)
to.deposit(amount)
}
}
Value Classes
@JvmInline
value class UserId(val value: Long)
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email" }
}
}
// Inlined to Long/String at compile time — zero overhead
fun findUser(id: UserId): User? = repository.findById(id.value)
// Type safety guaranteed
val userId = UserId(42L)
val email = Email("user@example.com")
// findUser(email) // Compile error!
10. Testing
Kotest
// BehaviorSpec (BDD style)
class UserServiceBehaviorSpec : BehaviorSpec({
val repository = mockk<UserRepository>()
val service = UserService(repository)
Given("a user exists in the database") {
val user = User(1, "Kim", "kim@test.com")
coEvery { repository.findById(1L) } returns user
When("findById is called with the user's ID") {
val result = service.findById(1L)
Then("it should return the user") {
result shouldBe user
}
}
}
Given("no user exists") {
coEvery { repository.findById(any()) } returns null
When("findById is called") {
val result = service.findById(999L)
Then("it should return null") {
result shouldBe null
}
}
}
})
// Property-based testing
class StringExtensionTest : StringSpec({
"toSlug should produce valid slugs" {
checkAll(Arb.string(1..100)) { input ->
val slug = input.toSlug()
slug shouldNotContain " "
slug shouldMatch Regex("[a-z0-9-]*")
}
}
})
MockK
// Coroutine mocking
coEvery { repository.findById(any()) } returns mockUser
coVerify { repository.save(match { it.name == "Kim" }) }
// Extension function mocking
mockkStatic("com.example.ExtensionsKt")
every { "test".toSlug() } returns "test-slug"
// Object mocking
mockkObject(DateUtils)
every { DateUtils.now() } returns fixedDate
Turbine (Flow Testing)
class UserViewModelTest : FunSpec({
test("loadUsers should emit Loading then Success") {
val viewModel = UserViewModel(mockRepository)
viewModel.uiState.test {
viewModel.loadUsers()
awaitItem() shouldBe UiState.Loading
val success = awaitItem()
success.shouldBeInstanceOf<UiState.Success<List<User>>>()
success.data.size shouldBe 3
cancelAndIgnoreRemainingEvents()
}
}
})
Coroutine Testing
class CoroutineTest : FunSpec({
test("structured concurrency cancellation") {
runTest {
var completed = false
val job = launch {
try {
delay(10_000)
completed = true
} catch (e: CancellationException) {
// cleanup
throw e
}
}
advanceTimeBy(5_000)
job.cancel()
job.join()
completed shouldBe false
}
}
})
11. Interview Questions and Quizzes
Top 15 Interview Questions
Q1: How does Kotlin's null safety work?
Kotlin distinguishes between nullable and non-nullable types at the type system level. String cannot be null, while String? can be null. It provides tools like safe call operator (?.), Elvis operator (?:), and non-null assertion (!!). The compiler enforces null checks to prevent runtime NullPointerExceptions.
Q2: What's the difference between a data class and a regular class?
A data class automatically generates equals(), hashCode(), toString(), copy(), and componentN() functions. It must have at least one parameter in the primary constructor and cannot be abstract, open, sealed, or inner. It's ideal for DTOs and value objects.
Q3: Explain use cases for sealed classes.
Sealed classes define a restricted class hierarchy. When all subtypes are handled in a when expression, no else branch is needed, ensuring compile-time safety. They're ideal for API response states (Success/Error/Loading), navigation events, and UI state representation.
Q4: What's the difference between coroutines and threads?
Coroutines are much lighter than threads. Thousands of coroutines can run on a single thread. Coroutines use cooperative multitasking (yielding at suspension points), while threads use preemptive multitasking. Structured concurrency makes lifecycle management easier.
Q5: What's the difference between launch and async?
launch returns a Job and is fire-and-forget with no result. async returns a Deferred<T> and its result can be obtained with await(). Use launch when no result is needed, and async when you need results from parallel operations.
Q6: What's the difference between Flow and Channel?
Flow is a cold stream that only executes when subscribed. Channel is a hot stream that transmits data regardless of subscribers. Flow is declarative with easy transformations, while Channel is imperative and used for inter-coroutine communication.
Q7: What should you watch out for when using Kotlin with Spring Boot?
The kotlin-spring plugin is needed to make classes open (for Spring AOP proxies). The kotlin-jpa plugin adds no-arg constructors. Jackson's kotlin-module supports data class serialization. For coroutines, WebFlux + kotlinx-coroutines-reactor is required.
Q8: How do you choose between Ktor and Spring Boot?
Ktor excels at being lightweight, Kotlin-native, and DSL-based configuration. It's suitable for microservices and lightweight environments like Lambda. Spring Boot has a vast ecosystem, enterprise features, and auto-configuration. It's better for large applications or existing Spring infrastructure.
Q9: How do you optimize recomposition in Jetpack Compose?
Use remember for value caching, derivedStateOf for derived state management, Stable/Immutable annotations for stability marking, key for list item identification, lambda stabilization (wrapping with remember), and minimizing state read scope.
Q10: How do you manage platform-specific dependencies in Kotlin Multiplatform?
Use the expect/actual mechanism. Declare expect in commonMain and provide actual implementations in each platform (androidMain/iosMain). Dependencies are managed in the dependencies block of each source set.
Q11: What's the relationship between inline functions and reified type parameters?
Due to JVM type erasure, generic type information isn't available at runtime in regular functions. Inline functions have their code inlined at the call site, so the reified keyword can preserve type information. This enables is T checks and T::class access.
Q12: Explain Kotlin delegation.
Class delegation (by keyword) delegates interface implementation to another object, implementing the decorator pattern without boilerplate. Property delegation (lazy, observable, map) reuses getter/setter logic.
Q13: Why is structured concurrency important?
When a parent coroutine is cancelled, all child coroutines are automatically cancelled. If one child fails, sibling coroutines are also cancelled. Coroutine lifecycle is bound to scope, preventing memory leaks and resource leaks.
Q14: What are the use cases and limitations of value classes?
Value classes provide type safety with zero runtime overhead. They're ideal for wrapper types like UserId and Email. Limitations: only one property allowed, can implement interfaces but not inherit, no var properties, no === comparison.
Q15: How do Kotlin DSLs work?
They're based on lambdas with receivers. The T.() -> Unit form allows calling T's members directly within the scope. This enables building domain-specific languages like Gradle build scripts, Ktor routing, and HTML builders.
5 Quizzes
Q1: What's the output of this code?
fun main() = runBlocking {
val result = coroutineScope {
val a = async { delay(100); 1 }
val b = async { delay(200); 2 }
a.await() + b.await()
}
println(result)
}
Answer: 3
Both async coroutines run in parallel. a returns 1 after 100ms, b returns 2 after 200ms. Total time is about 200ms, and the result is 1 + 2 = 3. coroutineScope waits until all children complete.
Q2: What's the difference between sealed interface and sealed class?
Answer:
A sealed class can have state (properties) but only supports single inheritance. A sealed interface cannot have state but supports multiple implementation. With sealed interfaces, subclasses can inherit from other classes while belonging to the sealed hierarchy, offering more flexibility. Sealed interfaces were introduced in Kotlin 1.5.
Q3: Find the problem in this code.
class MyViewModel : ViewModel() {
fun loadData() {
GlobalScope.launch {
val data = repository.fetchData()
_state.value = data
}
}
}
Answer:
Using GlobalScope means the coroutine continues running even after the ViewModel is destroyed. This causes memory leaks and unnecessary work. Use viewModelScope.launch instead, which automatically cancels when the ViewModel is destroyed. This code violates the structured concurrency principle.
Q4: What are the differences between let, run, apply, also, and with?
Answer:
| Function | Object Reference | Return Value | Use Case |
|---|---|---|---|
| let | it | Lambda result | Null check + transform |
| run | this | Lambda result | Object setup + compute result |
| apply | this | Receiver object | Object initialization |
| also | it | Receiver object | Side effects (logging, etc.) |
| with | this | Lambda result | Calling methods on existing object |
Q5: What's the difference between StateFlow and SharedFlow?
Answer:
StateFlow holds a current state value and immediately delivers the latest value to new subscribers. It always requires an initial value and ignores duplicate values. SharedFlow has no state and is used for event broadcasting. Buffering is controlled via the replay parameter. StateFlow is suitable for UI state, while SharedFlow is ideal for one-time events (navigation, toasts).
References
- Kotlin Official Documentation - Language reference and tutorials
- Kotlin Coroutines Guide - Official coroutines documentation
- Spring Boot Kotlin Support - Spring Kotlin guide
- Ktor Official Documentation - Ktor framework guide
- Jetpack Compose - Android Compose official docs
- Kotlin Multiplatform - KMP official guide
- Kotest Framework - Kotlin test framework
- MockK - Kotlin mocking library
- Turbine - Flow testing library
- Kotlin Coroutines Design Document - Coroutine design philosophy
- Android Developers - Kotlin - Android Kotlin guide
- Compose Multiplatform - JetBrains Compose Multiplatform
- Kotlin KEEP - Kotlin Evolution and Enhancement Process
- kotlinx.serialization - Serialization library
- Arrow - Kotlin functional programming library
- SQLDelight - KMP SQL library