MWCodebymw.de ↗
Kotlin

callbackFlow – Listener-APIs in einen Flow verwandeln

Android steckt voller Callback-APIs mit `register`/`unregister`. Mit `callbackFlow` machst du daraus einen ganz normalen Flow – inklusive automatischem Abmelden, wenn niemand mehr zuhört.

Halb Android besteht aus Callbacks: Sensoren, Standort, SharedPreferences, ConnectivityManager, Bluetooth. Immer dasselbe Muster – register…Listener beim Start, unregister…Listener beim Aufräumen. Und genau das Aufräumen vergisst man, wenn man es von Hand macht.

callbackFlow dreht das um: Du beschreibst einmal, wie an- und abgemeldet wird, und bekommst dafür einen Flow, den du wie jeden anderen behandeln kannst – mit map, debounce, collectAsStateWithLifecycle. Das Abmelden passiert automatisch, sobald der Sammler verschwindet.

Das Grundmuster

import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow

fun ConnectivityManager.netzStatus(): Flow<Boolean> = callbackFlow {
    val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) { trySend(true) }
        override fun onLost(network: Network)      { trySend(false) }
    }
    registerDefaultNetworkCallback(callback)          // anmelden

    awaitClose { unregisterNetworkCallback(callback) } // abmelden – IMMER
}

Drei Dinge machen das Muster aus:

  • trySend(...) statt send(...): Callbacks sind keine Coroutinen, dürfen also nicht warten. trySend gibt den Wert ohne Suspendieren weiter und verwirft ihn im Zweifel, statt zu blockieren.
  • awaitClose { … } ist Pflicht. Der Block hält den Flow offen und läuft genau dann, wenn der Sammler abbricht – normal, per Exception oder durch cancel(). Ohne awaitClose wirft callbackFlow beim ersten Sammeln eine IllegalStateException.
  • Der Rückgabetyp ist ein stinknormaler Flow. Alles, was du sonst mit Flows machst, geht auch hier.

Was der Callback zu schnell liefert

Sensoren feuern gern hundertmal pro Sekunde. Dagegen hilft ein Puffer mit klarer Regel statt eines wachsenden Rückstaus:

fun SensorManager.beschleunigung(sensor: Sensor): Flow<FloatArray> = callbackFlow {
    val listener = object : SensorEventListener {
        override fun onSensorChanged(e: SensorEvent) { trySend(e.values.clone()) }
        override fun onAccuracyChanged(s: Sensor, genauigkeit: Int) {}
    }
    registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI)
    awaitClose { unregisterListener(listener) }
}.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

DROP_OLDEST heißt: Der neueste Messwert gewinnt, alte fliegen raus. Bei Sensordaten ist das fast immer richtig – ein zwei Sekunden alter Wert nützt niemandem.

Wichtig ist auch das clone(). Android recycelt das values-Array zwischen den Aufrufen; wer die Referenz weiterreicht, bekommt Werte, die sich hinter seinem Rücken ändern. Das ist ein Fehler, den man beim Lesen nicht sieht und beim Debuggen eine Stunde sucht.

In Compose sammeln

@Composable
fun NetzHinweis(cm: ConnectivityManager) {
    val online by remember { cm.netzStatus() }
        .collectAsStateWithLifecycle(initialValue = true)

    if (!online) {
        Text("Offline – Änderungen werden später gesendet.")
    }
}

Verlässt der Screen die Anzeige, endet das Sammeln, awaitClose läuft, der Listener ist ab. Ohne dass du irgendwo ein onDestroy überschreiben musst.

Ein Hinweis noch, wenn mehrere Stellen denselben Flow brauchen: callbackFlow meldet sich pro Sammler einmal an. Zwei Composables heißt zwei Registrierungen. Willst du das nicht, hängst du ein .shareIn(scope, SharingStarted.WhileSubscribed(5000), replay = 1) dran – dann teilen sich alle eine Anmeldung.

Ich baue solche Brücken in fast jeder App, die mit Hardware oder Systemdiensten spricht. Wenn du eine Android-App in Kotlin & Compose planst, melde dich über bymw.de.

Quellen

#Flow#Coroutines#Callback#Android#Kotlin

Du brauchst mehr als ein Snippet?

Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.

Projekt anfragen →