Разработка системы лайков в мобильном приложении
Лайк кажется тривиальным: тап — инкремент счётчика — иконка заполнилась. Но без оптимистичного обновления кнопка «лагает» на 300-500ms ожидая ответа сервера, что субъективно убивает ощущение от приложения. А двойной тап или быстрые повторные нажатия без дебаунса генерируют лишние запросы и могут сломать счётчик.
Оптимистичное обновление
Стандарт для социальных приложений — обновлять UI мгновенно, не дожидаясь ответа сервера:
iOS (UIKit):
func toggleLike(for post: Post) {
let wasLiked = post.isLiked
// Мгновенно меняем UI
post.isLiked = !wasLiked
post.likesCount += wasLiked ? -1 : 1
updateCell(for: post)
// Запрос на сервер
apiService.toggleLike(postId: post.id) { [weak self] result in
if case .failure = result {
// Откат
post.isLiked = wasLiked
post.likesCount += wasLiked ? 1 : -1
self?.updateCell(for: post)
}
}
}
На Compose аналогично: likedState во ViewModel меняется сразу, запрос идёт параллельно, при ошибке — StateFlow откатывается к предыдущему значению.
Дебаунс и защита от спама
Быстрые двойные нажатия нужно защитить. Самый простой способ — флаг isRequesting: Bool на уровне ViewModel, блокирующий повторный вызов до получения ответа. Для более сложных случаев — debounce на 300ms: отправляем финальное состояние (liked/unliked), а не каждое нажатие.
На Android с Kotlin Flow:
likeButtonClicks
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { liked -> toggleLikeUseCase(postId, liked) }
.launchIn(viewModelScope)
Анимация
Анимация лайка — мелочь, которую пользователи замечают. Instagram-подход: сердечко «пружинит» при тапе. На iOS — UIView.animate(withDuration: 0.1, animations: { button.transform = CGAffineTransform(scaleX: 1.3, y: 1.3) }) { _ in UIView.animate(...) { button.transform = .identity } }. На Compose — animateFloatAsState с spring(dampingRatio = 0.4f).
Цвет заполненного лайка через tintColor (iOS) или ColorFilter.tint (Compose). Иконка — SF Symbol heart / heart.fill на iOS, Material Icon на Android.
Счётчик и агрегация
Хранить likes_count как денормализованное поле в таблице поста — правильно. Не считать SELECT COUNT(*) при каждом запросе ленты. Инкремент/декремент через атомарный UPDATE posts SET likes_count = likes_count + 1 WHERE id = ? — без race condition.
Уникальность лайка: таблица likes (user_id, post_id, PRIMARY KEY (user_id, post_id)). Дубли невозможны на уровне БД.
Сроки
Базовая реализация с оптимистичным обновлением и анимацией — 4 часа — 1 день в зависимости от платформы. Стоимость рассчитывается индивидуально.







