KotlinSeniorExperience
Как понять, что команда использует Kotlin идиоматично, а не переносит привычки из другого языка?
Идиоматичный Kotlin виден в коде: extension functions вместо utility-классов, sealed/data classes вместо if-instanceof, корутины с правильным scope, отсутствие !! в production-коде.
Сигналы идиоматичного Kotlin
1. API-дизайн с extension functions
Команда не создаёт XxxUtils и XxxHelper классы — вместо этого расширяет существующие типы:
// Не идиоматично: Utils-класс
object DateUtils {
fun formatForDisplay(date: LocalDate): String = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
}
// Идиоматично: extension function на типе
fun LocalDate.toDisplayString(): String = format(DateTimeFormatter.ISO_LOCAL_DATE)
fun LocalDate.isWeekend(): Boolean = dayOfWeek in setOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)
2. Sealed classes вместо enum + when-else
// Не идиоматично: String/enum с magic values
enum class Status { LOADING, SUCCESS, ERROR }
var errorMessage: String? = null // отдельное поле, связь неявная
// Идиоматично: sealed class несёт данные
sealed class UiState {
object Loading : UiState()
data class Success(val jobs: List<Job>) : UiState()
data class Error(val message: String, val retryable: Boolean) : UiState()
}
// Компилятор проверяет exhaustiveness:
when (state) {
is UiState.Loading -> showSpinner()
is UiState.Success -> showJobs(state.jobs)
is UiState.Error -> showError(state.message)
}
3. Scope functions по назначению
// Не идиоматично: цепочка временных переменных
val builder = AlertDialog.Builder(context)
builder.setTitle("Confirm")
builder.setMessage("Delete?")
builder.show()
// Идиоматично: apply для конфигурации с side effect
AlertDialog.Builder(context).apply {
setTitle("Confirm")
setMessage("Delete?")
}.show()
// let — для nullable pipeline
user?.email?.let { sendNotification(it) }
// run — для вычисления значения в блоке
val greeting = run {
val hour = LocalTime.now().hour
if (hour < 12) "Good morning" else "Good afternoon"
}
4. Корутины с правильным scope и cancellation
// Не идиоматично: GlobalScope, игнорирование отмены
GlobalScope.launch { loadData() }
// Идиоматично: scope привязан к lifecycle, отмена автоматическая
class JobViewModel : ViewModel() {
private val _jobs = MutableStateFlow<UiState>(UiState.Loading)
val jobs: StateFlow<UiState> = _jobs.asStateFlow()
init {
viewModelScope.launch {
repository.getJobs()
.catch { e -> _jobs.value = UiState.Error(e.message ?: "Unknown", true) }
.collect { _jobs.value = UiState.Success(it) }
}
}
}
5. Отсутствие !! в production-коде
Grep по !! в production-коде — быстрый аудит. Исключение: тесты, где NPE — корректный сигнал о провале.
6. Иммутабельные data classes с copy()
// Не идиоматично: mutable state
class Job {
var title: String = ""
var salary: Int = 0
}
// Идиоматично: val + copy для изменений
data class Job(val title: String, val salary: Int)
val updated = job.copy(salary = job.salary + 1000)
Подводные камни при аудите команды
- Наличие
run { ... }как заменаif-блока без явной причины — overengineering, признак неопытности. - Везде
applyвместоalso/let— команда не различает receiver-based и argument-based scope functions. - Data class с
varполями — нарушение immutability, часто перенесено из Java. - Отсутствие
flowOnв репозитории и явныйwithContextв ViewModel — нарушение разделения ответственности. - Companion object с тяжёлой логикой вместо top-level функций — Java-static мышление.
whenсelse -> throwдля sealed class — компилятор уже проверяет exhaustiveness, else избыточен.- Использование
@JvmStaticи@JvmFieldтам, где нет Java-interop — признак Java-to-Kotlin механического перевода. - Лямбды с многострочным телом вместо вынесенной именованной функции снижают читаемость — идиоматичный Kotlin не боится named functions.
What hurts your answer
- Сразу обвинять Kotlin, не проверив соседние слои системы
- Чинить симптом без минимального воспроизведения и evidence
- Не учитывать версии, конфигурацию, окружение и recent changes
What they're listening for
- Умеет локализовать проблему вокруг Kotlin
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения