androidengineers.Book a session

Data Synchronization

Realtime Sync (Firebase/WebSockets)

article20 minHard

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

FactorFirebase FirestoreRaw WebSocket
Offline supportBuilt-inManual implementation
ReconnectAutomaticMust implement
Setup timeMinutesHours
CostPer read/writeInfrastructure cost
FlexibilityLimited to Firestore modelFull protocol control
Security rulesDeclarative (Firebase Rules)Application-level
BidirectionalListeners onlyFull duplex
Best forModerate realtime, startup speedHigh-frequency data, custom protocols

Key Takeaways

  • Firestore addSnapshotListener + callbackFlow is 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

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Realtime Sync (Firebase/WebSockets) | Android System Design | Android Engineers