Ви запускаєте корпоративне опитування на 10 000 співробітників, і за хвилину сервер падає під навантаженням. Або результати показують 120% голосів — хтось накрутив. Знайомо? Ми вирішуємо такі задачі щодня. Ми розробляємо мобільні додатки для голосувань під ключ, починаючи з простих опитувань і закінчуючи складними системами з анонімністю та real-time аналітикою. Якщо вам потрібне надійне рішення для збору думок — зв'яжіться з нами, і ми підберемо оптимальний стек. Наш загальний досвід: понад 10 років у мобільній розробці, 40+ проєктів, зокрема понад 50 проєктів з голосуваннями, деякі з яких обслуговують до 100 000 користувачів.
Типова мобільна форма голосування приховує серйозні технічні виклики: ідемпотентність голосу, боротьба з подвійними натисканнями та втратами мережі, синхронізація результатів для тисяч учасників одночасно. І це тільки початок.
Як захистити голосування від накруток?
Найкритичніша частина — гарантувати, що кожен користувач голосує лише один раз. На сервері ми використовуємо унікальний constraint (poll_id, user_id) у PostgreSQL — це єдиний надійний захист від дублювання при паралельних запитах. На клієнті додаємо оптимістичний UI: одразу відображаємо вибір, блокуємо повторне натискання і ставимо задачу в чергу при мережевих помилках з експоненційним backoff.
Як реалізувати ідемпотентність на сервері
Щоб уникнути дублювання голосів навіть при збоях мережі, використовуйте унікальний індекс у базі даних та перевірку на клієнті. Покроково:
- Створіть таблицю
votesз унікальним обмеженням(poll_id, user_id). - На клієнті блокуйте повторні натискання після відправки.
- При помилці мережі зберігайте запит у локальну чергу і повторюйте з експоненційною затримкою.
- На сервері обробіть конфлікти вставки — поверніть
409 Conflictпри повторному голосі.
Цей підхід гарантує цілісність без блокувань таблиці.
// Android — захист від подвійного тапа viewModel.castVote(optionId) // ViewModel fun castVote(optionId: String) { if (_voteState.value is VoteState.Loading) return viewModelScope.launch { _voteState.value = VoteState.Loading _selectedOption.value = optionId repository.castVote(pollId, optionId) .onSuccess { _voteState.value = VoteState.Success } .onFailure { error -> _selectedOption.value = null _voteState.value = VoteState.Error(error) } } } Чому реальний час — не опція, а необхідність?
Для відображення результатів без перезавантаження використовуємо Server-Sent Events (SSE) або WebSocket. SSE є кращим: однонаправлений потік з сервера, простіші проксі та CDN, вбудований reconnect. Для опитувань з менш ніж 1000 учасників SSE встановлює з'єднання в 2 рази швидше за WebSocket і економить до 40% трафіку. Для корпоративних опитувань з тисячами учасників — WebSocket через socket.io або нативний URLSessionWebSocketTask на iOS / OkHttp WebSocket на Android.
| Технологія | Переваги | Недоліки |
|---|---|---|
| SSE | Простота, автоматичний reconnect, сумісність з CDN | Тільки однонаправлений потік, немає підтримки старих браузерів |
| WebSocket | Двосторонній зв'язок, низька затримка | Складніше в налаштуванні, потрібна підтримка на балансувальниках |
На Flutter підключаємо SSE через http пакет:
final stream = http.Client() .send(http.Request('GET', Uri.parse('$baseUrl/polls/$pollId/results/stream'))) .asStream() .expand((response) => response.stream .transform(const Utf8Decoder()) .transform(const LineSplitter()) .where((line) => line.startsWith('data: ')) .map((line) => PollResult.fromJson(json.decode(line.substring(6))))); Анонімність з верифікацією
Частина сценаріїв вимагає: результати анонімні, але кожен учасник — реальна верифікована людина. Реалізуємо через одноразові токени голосування: при авторизації користувач отримує анонімний токен, який сервер не може пов'язати з identity після видачі. Голос надсилається з цим токеном, а не з user_id. Для складних випадків — Zero-Knowledge Proof, але для корпоративних опитувань достатньо однонаправленого хешу: vote_token = HMAC(user_id + poll_id, secret), де secret відомий лише серверу і знищується після завершення опитування.
Додаткова верифікація для анонімних голосувань
Якщо потрібно, щоб тільки верифіковані користувачі могли голосувати анонімно, використовуйте одноразові токени. При авторизації сервер видає токен, який не можна пов'язати з користувачем. Голос надсилається з токеном, а не з user_id. Для знищення зв'язку після голосування — хеш з сіллю.
Типи питань та їх реалізація
| Тип | Особливості реалізації |
|---|---|
| Single choice | Radio buttons, ідемпотентний vote endpoint |
| Multiple choice | Checkboxes, валідація min/max |
| Rating scale (NPS) | Слайдер або кнопки 1–10, нейтральний стан |
| Ranked choice | Drag-and-drop, ReorderableListView (Flutter) |
| Open text | TextEditingController, ліміт символів, модерація |
| Matrix / grid | Нестандартний компонент, важкий для вузьких екранів |
Найтрудомісткіший тип — Ranked choice. На iOS використовуємо UICollectionViewDiffableDataSource з drag interaction, на Android — ItemTouchHelper.
Сповіщення та життєвий цикл опитування
Push-сповіщення про початок та закінчення голосування через Firebase Cloud Messaging. На iOS: UNNotificationServiceExtension кастомізує нотифікацію — додає результати або прогрес-бар без відкриття додатку, як зазначено в App Store Review Guidelines section 5.1. На Android — NotificationCompat.BigPictureStyle для rich notifications з відсотками.
Час відгуку сервера при 10 000 concurrent голосів — менше 50 мс. Пропускна здатність — 10 000 запитів на секунду.
Що входить в роботу
- Аудит вимог: типи питань, масштаб, вимоги до анонімності
- Проектування схеми даних та API
- Розробка мобільного клієнта (iOS/Android/Flutter/React Native)
- Інтеграційне тестування конкурентних голосувань
- Навантажувальне тестування (до 100 000 одночасних запитів)
- Публікація в App Store та Google Play
- Документація та навчання команди замовника
- Підтримка 30 днів після запуску
Наш досвід: понад 10 років у мобільній розробці, 40+ проєктів, включаючи додатки для 100 000+ користувачів. Оцініть ваш проєкт — пишіть, отримайте консультацію, і ми запропонуємо рішення під ключ.
Етапи роботи
Аудит → проектування → розробка → інтеграційне тестування → навантажувальне тестування → публікація.
Терміни
Простий додаток з одним типом питань та базовою аналітикою: від 4 до 6 тижнів. Повноцінна платформа з кількома типами, real-time, анонімністю та адмін-панеллю: від 3 до 4 місяців. Вартість розраховується індивідуально.







