Voting and Polling Mobile App Development
Voting in a mobile app is a task where technical complexity hides behind a simple interface. Real requirements: one user — one vote (preventing manipulation), results in real-time for thousands of simultaneous participants, correct behavior with unstable networks, sometimes anonymity with verified identity. This is not "a form with a button" — this is a system with data integrity requirements.
Voting Integrity: How Not to Get Hacked
The most critical part is vote idempotency. A user should not vote twice even with double-tap, network loss at the moment of sending, or app reinstallation.
On the client: optimistic UI — immediately show the user's choice, block repeat tap, send the vote to the server. On network error — queue it and retry with exponential backoff.
On the server: unique constraint (poll_id, user_id) in PostgreSQL — this is the only guarantee. Any application-level logic is insufficient with parallel requests.
// Android Kotlin — optimistic UI + double-tap protection
viewModel.castVote(optionId) // StateFlow: VoteState.Loading -> VoteState.Success/Error
// ViewModel
fun castVote(optionId: String) {
if (_voteState.value is VoteState.Loading) return // protect from repeat tap
viewModelScope.launch {
_voteState.value = VoteState.Loading
_selectedOption.value = optionId // optimistic UI update
repository.castVote(pollId, optionId)
.onSuccess { _voteState.value = VoteState.Success }
.onFailure { error ->
_selectedOption.value = null // rollback
_voteState.value = VoteState.Error(error)
}
}
}
Real-Time Results: No Page Reload
To display voting results in real-time, use WebSocket or Server-Sent Events. SSE is preferable for most cases: unidirectional stream from server, simpler with proxies and CDN, built-in reconnect.
On Flutter via http package or web_socket_channel:
// SSE for voting results
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)))));
For corporate polls with thousands of participants — WebSocket via socket.io or native URLSessionWebSocketTask on iOS / OkHttp WebSocket on Android.
Anonymity with Verification
Some scenarios require: results are anonymous, but each participant is a real verified person. This is implemented through voting tokens: during authorization, a user receives a one-time anonymous token that the server cannot link to identity after issuance. The vote is sent with this token, not the user_id.
A more complex variant — Zero-Knowledge Proof, but for most corporate polls, a simple one-way hash suffices: vote_token = HMAC(user_id + poll_id, secret), where secret is known only to the server and destroyed after the poll ends.
Question Types and Implementation
| Type | Implementation Specifics |
|---|---|
| Single choice | Radio buttons, idempotent vote endpoint |
| Multiple choice | Checkboxes, validation min_selections / max_selections |
| Rating scale (NPS) | Slider or buttons 1–10, neutral state by default |
| Ranked choice | Drag-and-drop, ReorderableListView on Flutter |
| Open text | TextEditingController with character limit, moderation |
| Matrix / grid | Non-standard component, heavy for narrow screens |
Drag-and-drop for Ranked choice is the most labor-intensive type: on iOS UICollectionViewDiffableDataSource with drag interaction, on Android ItemTouchHelper.
Notifications and Poll Lifecycle
Push notifications about poll start and end via Firebase Cloud Messaging. On iOS: UNNotificationServiceExtension allows customizing the notification directly from the push — add results or progress bar without opening the app. On Android — NotificationCompat.BigPictureStyle for rich notifications with percentages.
Work Phases
Requirements audit (question types, scale, anonymity requirements) → data schema and API design → mobile client development → integration testing of concurrent polls → load testing → publication.
Timeframe
Simple app with one question type and basic analytics: 4–6 weeks. Full platform with multiple question types, real-time, anonymity, and admin panel: 3–4 months. Cost calculated individually.







