SprintCode.pro
Подготовка к алгоритмическим задачам

Собеседование на Kotlin-разработчика: 30 популярных вопросов с ответами
Почему Kotlin востребован на рынке?
Kotlin — один из самых быстрорастущих языков программирования:
- 📱 Официальный язык для Android-разработки (Google, 2019)
- 🚀 Используется в Netflix, Pinterest, Uber, Trello, Coursera
- 💰 Высокие зарплаты для Kotlin-разработчиков
- 🏢 Востребован в Яндекс, Тинькофф, Сбер, Avito, VK
- 🔧 Подходит для Android, Backend (Ktor, Spring), Multiplatform
Структура собеседования на Kotlin-разработчика
- Основы Kotlin — синтаксис, типы, null-safety
- ООП и функциональное программирование — классы, лямбды, extension functions
- Корутины — асинхронное программирование
- Android-специфика — Activity lifecycle, Jetpack Compose (для Android)
- Практические задачи — алгоритмы, проектирование
Часть 1: Основы Kotlin
Вопрос 1: Чем Kotlin отличается от Java?
Ответ:
// 1. Null-safety на уровне типов var name: String = "Kotlin" // Не может быть null var nullable: String? = null // Может быть null // 2. Data classes — автогенерация equals, hashCode, toString, copy data class User(val name: String, val age: Int) // 3. Extension functions — расширение существующих классов fun String.addExclamation() = this + "!" "Hello".addExclamation() // "Hello!" // 4. Smart casts fun demo(x: Any) { if (x is String) { println(x.length) // x автоматически приведен к String } } // 5. Корутины вместо callbacks/RxJava suspend fun fetchData(): Data { return withContext(Dispatchers.IO) { api.getData() } } // 6. Лаконичный синтаксис val list = listOf(1, 2, 3) val doubled = list.map { it * 2 } // 7. Нет checked exceptions // 8. val/var вместо final // 9. when вместо switch // 10. Синглтоны через object
Вопрос 2: Как работает null-safety в Kotlin?
Ответ:
// Nullable типы обозначаются знаком ? var name: String = "Kotlin" // name = null // Ошибка компиляции! var nullable: String? = "Hello" nullable = null // OK // Safe call operator (?.) val length = nullable?.length // Int? — вернёт null если nullable == null // Elvis operator (?:) val len = nullable?.length ?: 0 // Если null, то 0 // Not-null assertion (!!) — опасно! val forcedLength = nullable!!.length // NullPointerException если null // Safe cast (as?) val str: String? = value as? String // null если cast невозможен // let для работы с nullable nullable?.let { nonNullValue -> println(nonNullValue.length) } // Проверка на null if (nullable != null) { println(nullable.length) // Smart cast к String } // lateinit для поздней инициализации (не nullable) lateinit var adapter: RecyclerView.Adapter<*> // lazy для ленивой инициализации val heavyObject: HeavyClass by lazy { HeavyClass() }
Вопрос 3: Разница между val и var? Что такое const?
Ответ:
// val — read-only (immutable reference) val list = mutableListOf(1, 2, 3) // list = mutableListOf(4, 5) // Ошибка! list.add(4) // OK — сам список мутабельный // var — mutable reference var counter = 0 counter = 1 // OK // const val — compile-time константа // Только примитивы и String, только top-level или в object const val API_URL = "https://api.example.com" const val MAX_COUNT = 100 // const vs val object Config { const val TIMEOUT = 5000 // Инлайнится в bytecode val startTime = System.currentTimeMillis() // Вычисляется в runtime } // val с custom getter — вычисляется каждый раз val currentTime: Long get() = System.currentTimeMillis() // Backing field var counter = 0 set(value) { if (value >= 0) field = value }
Пройди собеседование в топ-компанию
Платформа для подготовки
Решай алгоритмические задачи как профи
✓ Популярные алгоритмы✓ Разбор решений✓ AI помощь
Начать сейчас
Вопрос 4: Что такое data class?
Ответ:
// Data class автоматически генерирует: // - equals() / hashCode() // - toString() // - componentN() функции для деструктуризации // - copy() data class User( val id: Long, val name: String, val email: String ) fun main() { val user1 = User(1, "Alice", "alice@mail.com") val user2 = User(1, "Alice", "alice@mail.com") // equals() println(user1 == user2) // true (сравнение по содержимому) // toString() println(user1) // User(id=1, name=Alice, email=alice@mail.com) // copy() val user3 = user1.copy(name = "Bob") println(user3) // User(id=1, name=Bob, email=alice@mail.com) // Деструктуризация val (id, name, email) = user1 println("$name: $email") } // Ограничения data class: // - Должен иметь хотя бы один параметр в primary constructor // - Все параметры должны быть val или var // - Не может быть abstract, open, sealed или inner // Для наследования используйте обычные классы или sealed
Вопрос 5: Что такое sealed class и когда использовать?
Ответ:
// Sealed class — ограниченная иерархия классов // Все наследники известны на этапе компиляции sealed class Result<out T> { data class Success<T>(val data: T) : Result<T>() data class Error(val message: String) : Result<Nothing>() object Loading : Result<Nothing>() } fun handleResult(result: Result<String>) { // when исчерпывающий — компилятор проверяет все варианты when (result) { is Result.Success -> println("Data: ${result.data}") is Result.Error -> println("Error: ${result.message}") is Result.Loading -> println("Loading...") // Не нужен else — все случаи покрыты } } // Sealed interface (Kotlin 1.5+) sealed interface ApiResponse { data class Success(val body: String) : ApiResponse data class HttpError(val code: Int) : ApiResponse data class NetworkError(val cause: Throwable) : ApiResponse } // Практические применения: // 1. Состояния UI (Loading, Success, Error) // 2. События навигации // 3. Действия в Redux-подобных архитектурах // 4. Результаты операций sealed class UiState { object Initial : UiState() object Loading : UiState() data class Content(val items: List<Item>) : UiState() data class Error(val throwable: Throwable) : UiState() }
Часть 2: ООП и функциональное программирование
Вопрос 6: Как работают extension functions?
Ответ:
// Extension function — добавление метода к существующему классу fun String.removeWhitespace(): String { return this.replace("\\s".toRegex(), "") } "Hello World".removeWhitespace() // "HelloWorld" // Extension property val String.wordCount: Int get() = this.split("\\s+".toRegex()).size "Hello World Kotlin".wordCount // 3 // Extension на nullable тип fun String?.orEmpty(): String = this ?: "" val nullStr: String? = null nullStr.orEmpty() // "" // Под капотом — статическая функция // fun removeWhitespace(receiver: String): String // Extensions разрешаются статически! open class Animal class Dog : Animal() fun Animal.speak() = "Animal sound" fun Dog.speak() = "Woof!" fun makeSpeak(animal: Animal) { println(animal.speak()) // Всегда "Animal sound"! } makeSpeak(Dog()) // "Animal sound" — тип определяется на compile-time // Scope functions как extension inline fun <T, R> T.let(block: (T) -> R): R = block(this) inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this } inline fun <T, R> T.run(block: T.() -> R): R = block() inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
Вопрос 7: Объясните scope functions (let, run, with, apply, also)
Ответ:
data class User(var name: String, var age: Int) // let — трансформация, работа с nullable val user: User? = User("Alice", 25) val greeting = user?.let { "Hello, ${it.name}" } // it — объект // run — выполнение блока и возврат результата val result = user?.run { age += 1 // this — объект (можно опустить) "Updated $name to age $age" } // with — как run, но не extension val description = with(user!!) { "Name: $name, Age: $age" } // apply — конфигурация объекта, возвращает сам объект val configuredUser = User("Bob", 30).apply { name = name.uppercase() age += 5 } // also — побочные эффекты, возвращает сам объект val loggedUser = User("Charlie", 20).also { println("Created user: ${it.name}") } // Сравнение: // ┌──────────┬─────────────┬──────────────┐ // │ Функция │ Ссылка │ Возвращает │ // ├──────────┼─────────────┼──────────────┤ // │ let │ it │ Lambda result│ // │ run │ this │ Lambda result│ // │ with │ this │ Lambda result│ // │ apply │ this │ Context obj │ // │ also │ it │ Context obj │ // └──────────┴─────────────┴──────────────┘ // Типичные use cases: // let — null check, трансформация // run — вычисление с объектом // with — вызов методов объекта // apply — инициализация/конфигурация // also — логирование, валидация
Вопрос 8: Что такое inline functions и reified?
Ответ:
// inline — тело функции копируется в место вызова inline fun measureTime(block: () -> Unit): Long { val start = System.currentTimeMillis() block() return System.currentTimeMillis() - start } // Без inline каждая лямбда создает объект // С inline — нет оверхеда, код инлайнится // noinline — запрет инлайнинга конкретной лямбды inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { // notInlined можно сохранить в переменную } // crossinline — запрет non-local return inline fun runAsync(crossinline block: () -> Unit) { Thread { block() }.start() // return в block был бы небезопасен } // reified — доступ к типу в runtime inline fun <reified T> isInstance(value: Any): Boolean { return value is T // Без reified это невозможно! } isInstance<String>("hello") // true isInstance<Int>("hello") // false // Практический пример: Gson inline fun <reified T> Gson.fromJson(json: String): T { return fromJson(json, T::class.java) } val user: User = gson.fromJson(jsonString) // Тип выводится автоматически // Пример: startActivity в Android inline fun <reified T : Activity> Context.startActivity() { startActivity(Intent(this, T::class.java)) } context.startActivity<MainActivity>()
Вопрос 9: Как работает делегирование в Kotlin?
Ответ:
// Делегирование класса (by) interface Printer { fun print(message: String) } class ConsolePrinter : Printer { override fun print(message: String) = println(message) } // Делегирование реализации интерфейса class PrefixPrinter(printer: Printer) : Printer by printer { // Можно переопределить методы override fun print(message: String) { println("[PREFIX] $message") } } // Делегирование свойств class Example { // lazy — ленивая инициализация val heavyData: String by lazy { println("Computing...") "Heavy Result" } // observable — отслеживание изменений var name: String by Delegates.observable("Initial") { prop, old, new -> println("$old -> $new") } // vetoable — валидация перед изменением var age: Int by Delegates.vetoable(0) { _, _, newValue -> newValue >= 0 // Разрешить только неотрицательные } // notNull — lateinit для примитивов var count: Int by Delegates.notNull() // Делегирование в Map val map = mapOf("name" to "Alice", "age" to 25) val userName: String by map val userAge: Int by map } // Создание своего делегата class LoggingDelegate<T>(private var value: T) : ReadWriteProperty<Any?, T> { override fun getValue(thisRef: Any?, property: KProperty<*>): T { println("Getting ${property.name}: $value") return value } override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { println("Setting ${property.name}: $value") this.value = value } } var logged: String by LoggingDelegate("initial")
Вопрос 10: Что такое Higher-Order Functions?
Ответ:
// Higher-Order Function принимает или возвращает функцию // Функция как параметр fun operateOnNumbers(a: Int, b: Int, operation: (Int, Int) -> Int): Int { return operation(a, b) } val sum = operateOnNumbers(5, 3) { x, y -> x + y } // 8 val product = operateOnNumbers(5, 3) { x, y -> x * y } // 15 // Функция как возвращаемое значение fun multiplier(factor: Int): (Int) -> Int { return { number -> number * factor } } val triple = multiplier(3) triple(5) // 15 // Типы функций val noArgs: () -> Unit = { println("Hello") } val oneArg: (Int) -> String = { "Number: $it" } val twoArgs: (Int, Int) -> Int = { a, b -> a + b } val receiver: Int.() -> Int = { this * 2 } // Extension function type 5.receiver() // 10 // Trailing lambda fun repeat(times: Int, action: (Int) -> Unit) { for (i in 0 until times) action(i) } repeat(3) { index -> println("Iteration $index") } // Функциональные операции над коллекциями val numbers = listOf(1, 2, 3, 4, 5) numbers .filter { it % 2 == 0 } // [2, 4] .map { it * 10 } // [20, 40] .reduce { acc, i -> acc + i } // 60
Часть 3: Корутины (Coroutines)
Вопрос 11: Что такое корутины и зачем они нужны?
Ответ:
// Корутины — легковесные потоки для асинхронного программирования // Преимущества перед callbacks и RxJava: // - Читаемый последовательный код // - Легкое управление отменой // - Структурированная конкурентность import kotlinx.coroutines.* // Запуск корутины fun main() = runBlocking { // launch — fire-and-forget, возвращает Job val job = launch { delay(1000) println("World!") } println("Hello,") job.join() // Ждем завершения } // async — возвращает Deferred<T> с результатом suspend fun fetchUserData(): User { return coroutineScope { val user = async { api.getUser() } val friends = async { api.getFriends() } User(user.await(), friends.await()) } } // suspend function — может приостанавливаться без блокировки потока suspend fun loadData(): Data { delay(1000) // Не блокирует поток! return Data() } // Корутины vs Потоки // - 100,000 корутин — ОК // - 100,000 потоков — OutOfMemoryError fun main() = runBlocking { repeat(100_000) { launch { delay(1000) print(".") } } }
Вопрос 12: Что такое CoroutineScope и CoroutineContext?
Ответ:
// CoroutineContext — набор элементов (Job, Dispatcher, Name, etc.) // CoroutineScope — область жизни корутин // Основные диспатчеры Dispatchers.Main // UI поток (Android) Dispatchers.IO // Сетевые/дисковые операции Dispatchers.Default // CPU-интенсивные задачи Dispatchers.Unconfined // Без привязки к потоку // Создание scope val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) // Переключение контекста suspend fun loadAndDisplay() { val data = withContext(Dispatchers.IO) { // Выполняется в IO потоке api.fetchData() } // Здесь уже в оригинальном контексте display(data) } // Android: viewModelScope, lifecycleScope class MyViewModel : ViewModel() { fun loadData() { viewModelScope.launch { // Автоматически отменяется при onCleared() val result = repository.getData() _uiState.value = result } } } // Structured concurrency — дочерние корутины привязаны к родителю coroutineScope { launch { task1() } launch { task2() } // Ждет завершения обоих } // Если одна упадет — отменятся все // SupervisorJob — ошибка в одной не отменяет другие supervisorScope { launch { task1() } // Падает launch { task2() } // Продолжает работать }
Вопрос 13: Как обрабатывать ошибки в корутинах?
Ответ:
// 1. try-catch внутри корутины launch { try { riskyOperation() } catch (e: Exception) { handleError(e) } } // 2. CoroutineExceptionHandler val handler = CoroutineExceptionHandler { _, exception -> println("Caught: $exception") } val scope = CoroutineScope(Dispatchers.IO + handler) scope.launch { throw RuntimeException("Oops!") } // 3. supervisorScope для изоляции ошибок supervisorScope { val job1 = launch { throw Exception("Failed!") } val job2 = launch { delay(1000) println("Still running") // Выполнится! } } // 4. async + await обработка val deferred = async { throw Exception("Async error") } try { deferred.await() } catch (e: Exception) { println("Caught: $e") } // 5. Result wrapper suspend fun safeCall(): Result<Data> { return try { Result.success(api.getData()) } catch (e: Exception) { Result.failure(e) } } // 6. runCatching val result = runCatching { riskyOperation() } .onSuccess { println("Success: $it") } .onFailure { println("Error: $it") } .getOrDefault(defaultValue)
Вопрос 14: Что такое Flow и чем отличается от LiveData?
Ответ:
// Flow — холодный асинхронный поток данных fun numbersFlow(): Flow<Int> = flow { for (i in 1..5) { delay(100) emit(i) // Отправляем значение } } // Сбор данных suspend fun main() { numbersFlow().collect { value -> println(value) } } // Операторы Flow numbersFlow() .filter { it % 2 == 0 } .map { it * 10 } .onEach { println("Processing: $it") } .catch { e -> println("Error: $e") } .collect { println("Result: $it") } // StateFlow — горячий поток с текущим значением (замена LiveData) class MyViewModel : ViewModel() { private val _uiState = MutableStateFlow<UiState>(UiState.Loading) val uiState: StateFlow<UiState> = _uiState.asStateFlow() fun loadData() { viewModelScope.launch { _uiState.value = UiState.Loading try { val data = repository.getData() _uiState.value = UiState.Success(data) } catch (e: Exception) { _uiState.value = UiState.Error(e) } } } } // SharedFlow — для событий (в отличие от StateFlow, не хранит значение) private val _events = MutableSharedFlow<Event>() val events: SharedFlow<Event> = _events.asSharedFlow() // Flow vs LiveData: // ┌─────────────┬─────────────────┬─────────────────┐ // │ │ Flow │ LiveData │ // ├─────────────┼─────────────────┼─────────────────┤ // │ Платформа │ Kotlin (любая) │ Android только │ // │ Операторы │ Много (map,etc) │ Мало │ // │ Threading │ Гибкий │ Main thread │ // │ Холодный │ Да (Flow) │ Нет │ // │ Горячий │ StateFlow │ Да │ // └─────────────┴─────────────────┴─────────────────┘ // Сбор Flow в UI (Android) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> updateUI(state) } } }
Вопрос 15: Как работает отмена корутин?
Ответ:
// Корутины кооперативно отменяемы val job = launch { repeat(1000) { i -> println("Job: $i") delay(100) // Проверяет отмену } } delay(500) job.cancel() // Отменяем job.join() // Ждем завершения // Проверка отмены вручную val job = launch { while (isActive) { // Проверяем флаг // CPU-intensive работа } } // ensureActive() — выбрасывает CancellationException launch { while (true) { ensureActive() heavyComputation() } } // yield() — приостановка и проверка отмены launch { while (true) { yield() heavyComputation() } } // Non-cancellable код launch { try { work() } finally { withContext(NonCancellable) { // Этот код выполнится даже при отмене cleanup() } } } // Таймаут withTimeout(1000) { // Выбросит TimeoutCancellationException longRunningOperation() } withTimeoutOrNull(1000) { longRunningOperation() } ?: println("Timeout!") // cancelAndJoin() — отмена и ожидание job.cancelAndJoin()
Часть 4: Android-специфика
Вопрос 16: Жизненный цикл Activity и Fragment
Ответ:
// Activity Lifecycle class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Инициализация UI, восстановление состояния } override fun onStart() { super.onStart() // Activity видима } override fun onResume() { super.onResume() // Activity в фокусе, пользователь взаимодействует } override fun onPause() { super.onPause() // Частично перекрыта, сохраняем данные } override fun onStop() { super.onStop() // Не видима, освобождаем ресурсы } override fun onDestroy() { super.onDestroy() // Уничтожается (поворот экрана или finish()) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString("key", value) } } // Правильная работа с lifecycleScope class MyFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Автоматически отменяется при onDestroyView viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> updateUI(state) } } } } } // ViewModel переживает смену конфигурации class MyViewModel : ViewModel() { private val _data = MutableStateFlow<Data?>(null) val data: StateFlow<Data?> = _data.asStateFlow() init { loadData() } private fun loadData() { viewModelScope.launch { _data.value = repository.getData() } } override fun onCleared() { super.onCleared() // Очистка ресурсов } }
Вопрос 17: Jetpack Compose основы
Ответ:
// Composable функция @Composable fun Greeting(name: String, modifier: Modifier = Modifier) { Text( text = "Hello, $name!", modifier = modifier.padding(16.dp) ) } // State в Compose @Composable fun Counter() { var count by remember { mutableStateOf(0) } Column { Text("Count: $count") Button(onClick = { count++ }) { Text("Increment") } } } // remember vs rememberSaveable @Composable fun InputField() { // remember — переживает recomposition var text by remember { mutableStateOf("") } // rememberSaveable — переживает смену конфигурации var savedText by rememberSaveable { mutableStateOf("") } TextField(value = text, onValueChange = { text = it }) } // State hoisting — подъем состояния @Composable fun StatelessCounter( count: Int, onIncrement: () -> Unit ) { Button(onClick = onIncrement) { Text("Count: $count") } } @Composable fun StatefulCounter() { var count by remember { mutableStateOf(0) } StatelessCounter(count = count, onIncrement = { count++ }) } // Side effects @Composable fun MyScreen(viewModel: MyViewModel) { // LaunchedEffect — запуск корутины LaunchedEffect(Unit) { viewModel.loadData() } // collectAsState — подписка на Flow val uiState by viewModel.uiState.collectAsState() // DisposableEffect — очистка ресурсов DisposableEffect(Unit) { val listener = createListener() onDispose { listener.remove() } } }
Часть 5: Практические задачи
Вопрос 18: Реализуйте безопасный синглтон
Ответ:
// object — самый простой способ (thread-safe) object DatabaseManager { fun query(sql: String): List<Row> = TODO() } // С параметрами — Double-checked locking class Singleton private constructor(val config: Config) { companion object { @Volatile private var instance: Singleton? = null fun getInstance(config: Config): Singleton { return instance ?: synchronized(this) { instance ?: Singleton(config).also { instance = it } } } } } // Через lazy class LazySingleton private constructor() { companion object { val instance: LazySingleton by lazy { LazySingleton() } } }
Вопрос 19: Напишите extension для retry с exponential backoff
Ответ:
suspend fun <T> retry( times: Int = 3, initialDelay: Long = 100, maxDelay: Long = 10000, factor: Double = 2.0, block: suspend () -> T ): T { var currentDelay = initialDelay repeat(times - 1) { attempt -> try { return block() } catch (e: Exception) { println("Attempt ${attempt + 1} failed: ${e.message}") } delay(currentDelay) currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) } return block() // Последняя попытка } // Использование suspend fun fetchWithRetry(): Data { return retry(times = 5, initialDelay = 1000) { api.fetchData() } } // Версия с Flow fun <T> Flow<T>.retryWithExponentialBackoff( times: Int = 3, initialDelay: Long = 100 ): Flow<T> = retryWhen { cause, attempt -> if (attempt < times) { delay(initialDelay * (1 shl attempt.toInt())) true } else { false } }
Вопрос 20: Реализуйте простой кэш с LRU
Ответ:
class LRUCache<K, V>(private val capacity: Int) { private val cache = LinkedHashMap<K, V>(capacity, 0.75f, true) @Synchronized fun get(key: K): V? = cache[key] @Synchronized fun put(key: K, value: V) { if (cache.size >= capacity && !cache.containsKey(key)) { val eldest = cache.keys.first() cache.remove(eldest) } cache[key] = value } @Synchronized fun remove(key: K): V? = cache.remove(key) @Synchronized fun clear() = cache.clear() val size: Int get() = cache.size } // Thread-safe версия с корутинами class CoroutineLRUCache<K, V>(private val capacity: Int) { private val cache = LinkedHashMap<K, V>(capacity, 0.75f, true) private val mutex = Mutex() suspend fun get(key: K): V? = mutex.withLock { cache[key] } suspend fun put(key: K, value: V) = mutex.withLock { if (cache.size >= capacity && !cache.containsKey(key)) { cache.remove(cache.keys.first()) } cache[key] = value } }
Часто встречающиеся ошибки
Ошибка 1: Неправильное использование !!
// ПЛОХО val length = nullableString!!.length // NPE риск // ХОРОШО val length = nullableString?.length ?: 0 // или nullableString?.let { str -> processString(str) }
Ошибка 2: Утечка корутин
// ПЛОХО — корутина не привязана к lifecycle class MyActivity : AppCompatActivity() { fun loadData() { GlobalScope.launch { // Утечка! val data = api.getData() updateUI(data) } } } // ХОРОШО class MyActivity : AppCompatActivity() { fun loadData() { lifecycleScope.launch { val data = api.getData() updateUI(data) } } }
Ошибка 3: Блокировка Main потока
// ПЛОХО fun loadData() { val data = runBlocking { api.getData() } // Блокирует! } // ХОРОШО fun loadData() { viewModelScope.launch { val data = withContext(Dispatchers.IO) { api.getData() } _uiState.value = data } }
Заключение
Подготовка к собеседованию на Kotlin-разработчика требует понимания:
- Null-safety и идиоматичный Kotlin
- Корутины и Flow для асинхронности
- Extension functions и scope functions
- Sealed classes и делегирование
- Android Lifecycle и Jetpack Compose (для Android)
- Практические паттерны и best practices
Kotlin ценит краткость, безопасность и выразительность. На собеседовании важно показать не только знание синтаксиса, но и умение писать идиоматичный код.
