Утро начинается не с кофе, а с Merge Request'а
7:45 утра. Телефон вибрирует — это не будильник, это GitLab прислал уведомление: ваш вчерашний Merge Request прошёл CI/CD пайплайн. Пока чайник закипает, я открываю ноутбук и вижу зелёную галочку. Первая победа дня. Доброе утро, Kotlin.
Меня зовут Алексей, я Kotlin-разработчик в продуктовой компании. Последние три года я пишу на Kotlin под Android и немного на бэкенде с Ktor. Если вы думаете, что работа программиста — это бесконечное "Hello World", то сегодня я разрушу этот миф. Добро пожаловать в мой реальный рабочий день.
8:30 — Daily Standup: синхронизация и контекст
Первое, что происходит — ежедневный стендап. Мы собираемся в Zoom, но без лишних "вчера-сегодня-проблемы". У нас лаконично: что сделано, что в плане, есть ли блокеры. Сегодня я работаю над фичей Push-уведомлений для пользователей.
— Вчера закончил рефакторинг NotificationService, переписал на корутины. Сегодня подключаю Firebase Cloud Messaging и тестирую Flow. Блокеров нет.
Кстати, именно на стендапах часто рождаются лучшие идеи по архитектуре. Например, на прошлой неделе коллега предложил заменить RxJava на Kotlin Flow — и мы сократили количество boilerplate кода на 40%. Вот как это выглядит сейчас:
// Вместо старого RxJava:
// Observable.just(data).subscribeOn(Schedulers.io())
// Используем Kotlin Flow:
fun observeNotifications(): Flow<Notification> = flow {
val service = NotificationService()
while (true) {
emit(service.getNextNotification())
delay(5000) // опрос каждые 5 секунд
}
}.flowOn(Dispatchers.IO)
Разница очевидна: читаемость, безопасность типов и никакого callback hell.
9:15 — Разработка: пишем код на Kotlin
После стендапа я ныряю в код. Сегодня задача — реализовать обработку входящих push-уведомлений. Использую Kotlin Coroutines и StateFlow для управления состоянием UI. Вот как выглядит мой ViewModel:
class PushViewModel(
private val repository: NotificationRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<PushUiState>(PushUiState.Loading)
val uiState: StateFlow<PushUiState> = _uiState.asStateFlow()
fun loadNotifications() {
viewModelScope.launch {
repository.getNotifications()
.catch { e ->
_uiState.value = PushUiState.Error(e.message ?: "Unknown error")
}
.collect { notifications ->
_uiState.value = PushUiState.Success(notifications)
}
}
}
}
sealed class PushUiState {
object Loading : PushUiState()
data class Success(val data: List<Notification>) : PushUiState()
data class Error(val message: String) : PushUiState()
}
Обратите внимание: я использую sealed class для UI-состояния. Это стандартная практика в современной Android-разработке на Kotlin. Никаких if-else цепочек — только чистая типизация.
Кстати, если вы новичок в Kotlin, советую обратить внимание на Scope functions: let, apply, also. Они делают код лаконичнее. Например:
// Без scope functions
val user = User()
user.name = "Alex"
user.age = 30
saveUser(user)
// С apply
val user = User().apply {
name = "Alex"
age = 30
}
saveUser(user)
11:00 — Code Review: учимся у коллег
После обеда (да, я обедаю в 11 — привычка с удалёнки) я открываю Merge Request от коллеги. Он переписывает модуль авторизации на Kotlin Multiplatform. Читаю код и вижу интересный момент:
// Было (только Android):
class AuthRepository(private val api: ApiService) {
suspend fun login(email: String, password: String): Result<User>
}
// Стало (KMP):
expect class AuthRepository {
suspend fun login(email: String, password: String): Result<User>
}
// В Android-модуле:
actual class AuthRepository(private val api: ApiService) {
actual suspend fun login(email: String, password: String): Result<User> {
// логика для Android
}
}
Я комментирую: "Отличное использование expect/actual, но давай добавим sealed class Result вместо стандартного kotlin.Result — будет безопаснее". Коллега соглашается. Code review — это не критика, это совместное улучшение кода.
13:00 — Бэкенд на Ktor: микросервис на Kotlin
Во второй половине дня я переключаюсь на бэкенд. Мы используем Ktor — фреймворк от JetBrains. Пишу простой эндпоинт для получения списка уведомлений:
fun Application.notificationModule() {
routing {
get("/notifications") {
val userId = call.parameters["userId"] ?: return@get call.respondText(
"Missing userId",
status = HttpStatusCode.BadRequest
)
val notifications = notificationService.getByUserId(userId)
call.respond(notifications)
}
}
}
Ktor на Kotlin — это элегантно. Никакой магии аннотаций, всё явно. Если вы пишете на Kotlin бэкенд, рекомендую попробовать Ktor вместо Spring Boot — он легче и быстрее стартует.
15:00 — Тестирование: пишем Unit-тесты
Без тестов никуда. Я пишу тесты на Kotlin Test для своей ViewModel. Использую Turbine — библиотеку для тестирования Flow:
@Test
fun `when load notifications success then show success state`() = runTest {
val repository = FakeNotificationRepository()
val viewModel = PushViewModel(repository)
viewModel.loadNotifications()
val state = viewModel.uiState.take(2).toList()
assertTrue(state[0] is PushUiState.Loading)
assertTrue(state[1] is PushUiState.Success)
}
Тесты на Kotlin пишутся с удовольствием. Kotlin Test + MockK — идеальное комбо. Кстати, MockK понимает корутины из коробки, в отличие от Mockito.
17:00 — Рефакторинг и документация
Последний час я посвящаю рефакторингу. Нашёл в коде старый Listener на Java, который пора переписать на Kotlin:
// Java (старый код):
public interface OnDataLoadedListener {
void onSuccess(List<Data> data);
void onError(Exception e);
}
// Kotlin:
typealias OnDataLoaded = (Result<List<Data>>) -> Unit
Одна строка вместо целого интерфейса. Kotlin — это язык,