A tiny Kotlin utility that makes it structurally impossible to leak lifecycle-bound listeners.
Android apps that hold listener-based references — Firestore snapshot listeners, broadcast receivers, custom callbacks — need that listener cleaned up at the right time. Miss it, or clean up in the wrong order, and you get leaked listeners: stale references still firing after a screen rotation, a navigation event, or a ViewModel being cleared.
This is a common, well-known bug class. It's also easy to reintroduce even when you know about it, because the fix (track the listener, remember to unregister it, do it in the right lifecycle callback) is manual and easy to get subtly wrong.
Latch makes this structural instead of manual: it guarantees that starting a new listener always cleans up the previous one first, and that clearing always unregisters whatever's currently active — so the leak simply can't happen.
Add JitPack to your root settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}Then add the dependency:
dependencies {
implementation("com.github.shipframe:latch:1.0.0")
}class ChatViewModel(private val db: FirebaseFirestore, private val chatId: String) : ViewModel() {
private val chatListener = LatchRef {
val registration = db.collection("chats")
.document(chatId)
.addSnapshotListener { snapshot, _ -> updateUi(snapshot) }
Unregisterable { registration.remove() }
}
fun startListening() {
chatListener.get()
}
override fun onCleared() {
chatListener.clear()
}
}get() starts (or restarts) the listener — if one is already active, it's cleaned up first. clear() guarantees whatever's currently active gets unregistered, and is safe to call even if nothing was ever started.
LatchRef<T : Unregisterable>(create: () -> T)
Wraps a factory function that produces something cleanable.
.get(): T
Unregisters the current instance (if any), creates a new one via create(), stores it, and returns it.
.clear()
Unregisters the current instance (if any) and resets internal state. Safe to call multiple times or when nothing is active.
Unregisterable
A functional interface with a single method, unregister(). Wrap any cleanup logic in it:
Unregisterable { someApi.stopListening() }This started as a fix for a real bug: a Firestore listener that wasn't being removed on screen rotation. Each rotation created a new listener without clearing the old one — after a few rotations, the app held multiple active listeners writing to the same UI state, and eventually crashed on a stale reference.
The fix wasn't really "remember to call remove()" — it was making that cleanup structurally automatic. Latch is that fix, generalized beyond Firestore to anything with a cleanup step.
MIT — see LICENSE for details.