Polling is wasteful and slow for truly real-time data. Firebase Realtime Database/Firestore and WebSockets both push data to the client as it changes — but they have very different trade-offs.
Firebase Firestore: Declarative Realtime
Firestore gives you persistent listeners that survive network interruptions, offline caching, and automatic reconnect:
class ArticleRepository(private val firestore: FirebaseFirestore) {
fun getArticle(id: String): Flow<Article> = callbackFlow {
val listener = firestore.collection("articles")
.document(id)
.addSnapshotListener { snapshot, error ->
if (error != null) {
close(error) // terminate the flow on error
return@addSnapshotListener
}
snapshot?.toObject(Article::class.java)?.let { trySend(it) }
}
awaitClose { listener.remove() } // clean up when flow is cancelled
}
fun getArticlesList(authorId: String): Flow<List<Article>> = callbackFlow {
val listener = firestore.collection("articles")
.whereEqualTo("authorId", authorId)
.orderBy("createdAt", Query.Direction.DESCENDING)
.addSnapshotListener { snapshot, error ->
if (error != null) { close(error); return@addSnapshotListener }
val articles = snapshot?.toObjects(Article::class.java) ?: emptyList()
trySend(articles)
}
awaitClose { listener.remove() }
}
}
Firestore Offline Persistence
// Enable in Application.onCreate — do this once
FirebaseFirestore.getInstance().firestoreSettings = firestoreSettings {
isPersistenceEnabled = true
cacheSizeBytes = 50L * 1024 * 1024 // 50 MB
}
With persistence enabled, Firestore serves data from the local cache when offline, and syncs queued writes when reconnected.
WebSocket: Low-Level Realtime
WebSocket gives you full control at the cost of implementing protocol, reconnection, and message parsing yourself.
class RealtimeSyncClient(private val url: String) {
private val client = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS) // no read timeout for persistent connections
.build()
private var webSocket: WebSocket? = null
private val _messages = MutableSharedFlow<SyncMessage>()
val messages: SharedFlow<SyncMessage> = _messages.asSharedFlow()
fun connect() {
val request = Request.Builder().url(url).build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
println("WebSocket connected")
}
override fun onMessage(webSocket: WebSocket, text: String) {
val message = json.decodeFromString<SyncMessage>(text)
_messages.tryEmit(message)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
// Reconnect after backoff
scheduleReconnect()
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
if (code != 1000) scheduleReconnect() // 1000 = normal close
}
})
}
fun send(message: SyncMessage) {
webSocket?.send(json.encodeToString(message))
}
fun disconnect() = webSocket?.close(1000, "Client disconnecting")
private fun scheduleReconnect() {
// Exponential backoff reconnect
}
}
Applying WebSocket Updates to Room
class ArticleViewModel(
private val dao: ArticleDao,
private val wsClient: RealtimeSyncClient
) : ViewModel() {
init {
viewModelScope.launch {
wsClient.messages.collect { message ->
when (message.type) {
"article_updated" -> dao.insert(message.article.toEntity())
"article_deleted" -> dao.deleteById(message.articleId)
}
}
}
}
}
Firebase vs WebSocket: Decision Matrix
| Factor | Firebase Firestore | Raw WebSocket |
|---|---|---|
| Offline support | Built-in | Manual implementation |
| Reconnect | Automatic | Must implement |
| Setup time | Minutes | Hours |
| Cost | Per read/write | Infrastructure cost |
| Flexibility | Limited to Firestore model | Full protocol control |
| Security rules | Declarative (Firebase Rules) | Application-level |
| Bidirectional | Listeners only | Full duplex |
| Best for | Moderate realtime, startup speed | High-frequency data, custom protocols |
Key Takeaways
- Firestore
addSnapshotListener+callbackFlowis the idiomatic Android pattern for declarative realtime - Always
awaitClose { listener.remove() }to prevent listener leaks in Flow - Enable Firestore persistence for offline reads; writes are queued automatically
- For WebSocket: set
readTimeout(0)on OkHttp; implement reconnect with exponential backoff - Layer WebSocket updates on top of a Room cache — never read directly from the WebSocket stream in the UI