StateFlow und SharedFlow

StateFlow und SharedFlow sind Flow-APIs, mit denen Flows Statusaktualisierungen und Werte optimal an mehrere Consumer senden können.

StateFlow

StateFlow ist ein beobachtbarer Flow, der den aktuellen und neuen Status an seine Collector ausgibt. Der aktuelle Statuswert kann auch über die Property value gelesen werden. Wenn Sie den Status aktualisieren und an den Ablauf senden möchten, weisen Sie der Property value der Klasse MutableStateFlow einen neuen Wert zu.

In Android eignet sich StateFlow hervorragend für Klassen, die einen beobachtbaren, veränderlichen Zustand beibehalten müssen.

Anhand der Beispiele aus Kotlin-Flows kann ein StateFlow über das LatestNewsViewModel bereitgestellt werden, sodass das View auf Aktualisierungen des UI-Status warten kann und der Bildschirmstatus Konfigurationsänderungen übersteht.

class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    // Backing property to avoid state updates from other classes
    private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
    // The UI collects from this StateFlow to get its state updates
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    init {
        viewModelScope.launch {
            newsRepository.favoriteLatestNews
                // Update UI with the latest favorite news
                // Writes to the value property of MutableStateFlow,
                // adding a new element to the flow and updating all
                // of its collectors
                .collect { favoriteNews ->
                    _uiState.value = LatestNewsUiState.Success(favoriteNews)
                }
        }
    }
}

// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
    data class Success(val news: List<ArticleHeadline>) : LatestNewsUiState()
    data class Error(val exception: Throwable) : LatestNewsUiState()
}

Die Klasse, die für die Aktualisierung eines MutableStateFlow verantwortlich ist, ist der Producer. Alle Klassen, die Daten aus dem StateFlow abrufen, sind die Consumer. Im Gegensatz zu einem kalten Flow, der mit dem flow-Builder erstellt wurde, ist ein StateFlow heiß: Beim Erfassen aus dem Flow wird kein Erstellercode ausgelöst. Ein StateFlow ist immer aktiv und im Arbeitsspeicher. Es kommt erst für die Garbage Collection infrage, wenn keine anderen Verweise darauf von einem Garbage Collection-Root vorhanden sind.

Wenn ein neuer Consumer mit dem Erfassen von Daten aus dem Stream beginnt, empfängt er den letzten Status im Stream und alle nachfolgenden Status. Dieses Verhalten ist auch in anderen beobachtbaren Klassen wie LiveData zu finden.

Der View wartet wie bei jedem anderen Ablauf auf StateFlow:

class LatestNewsActivity : ComponentActivity() {
    private val latestNewsViewModel: LatestNewsViewModel = TODO() // getViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        // Start a coroutine in the lifecycle scope
        lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // Note that this happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                latestNewsViewModel.uiState.collect { uiState ->
                    // New value received
                    when (uiState) {
                        is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                        is LatestNewsUiState.Error -> showError(uiState.exception)
                    }
                }
            }
        }
    }
}

Wenn Sie einen beliebigen Ablauf in ein StateFlow konvertieren möchten, verwenden Sie den Zwischenoperator stateIn.

StateFlow, Flow und LiveData

StateFlow und LiveData haben Ähnlichkeiten. Beide sind Klassen für beobachtbare Dateninhaber und folgen einem ähnlichen Muster, wenn sie in Ihrer App-Architektur verwendet werden.

StateFlow und LiveData verhalten sich jedoch unterschiedlich:

  • Für StateFlow muss ein Anfangszustand an den Konstruktor übergeben werden, für LiveData nicht.
  • LiveData.observe() hebt die Registrierung des Nutzers automatisch auf, wenn der View in den Status STOPPED wechselt. Wenn Daten über StateFlow oder einen anderen Ablauf erhoben werden, wird die Erhebung nicht automatisch beendet. Um dasselbe Verhalten zu erzielen, müssen Sie den Flow aus einem Lifecycle.repeatOnLifecycle-Block erfassen.

Kalte Flows mit shareIn in heiße Flows umwandeln

StateFlow ist ein Hot-Flow. Er bleibt im Arbeitsspeicher, solange er erfasst wird oder solange andere Referenzen darauf von einem automatische Speicherbereinigung-Root aus vorhanden sind. Mit dem Operator shareIn können Sie kalte Flows in heiße Flows umwandeln.

Anhand des in Kotlin-Flows erstellten callbackFlow können Sie die aus Firestore abgerufenen Daten zwischen Collectors teilen, indem Sie shareIn verwenden. So muss nicht für jeden Collector ein neuer Flow erstellt werden. Sie müssen Folgendes übergeben:

  • Eine CoroutineScope, die zum Freigeben des Flows verwendet wird. Dieser Bereich sollte länger als jeder Verbraucher aktiv sein, damit der freigegebene Ablauf so lange wie nötig aktiv bleibt.
  • Die Anzahl der Elemente, die für jeden neuen Collector wiedergegeben werden sollen.
  • Die Richtlinie zum Startverhalten.

class NewsRemoteDataSource(
    private val externalScope: CoroutineScope
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        // ...
    }.shareIn(
        externalScope,
        replay = 1,
        started = SharingStarted.WhileSubscribed()
    )
}

In diesem Beispiel gibt der latestNews-Ablauf das zuletzt ausgegebene Element an einen neuen Collector aus und bleibt aktiv, solange externalScope aktiv ist und aktive Collectors vorhanden sind. Die SharingStarted.WhileSubscribed()-Startrichtlinie hält den Upstream-Produzenten aktiv, solange aktive Abonnenten vorhanden sind. Es sind auch andere Startrichtlinien verfügbar, z. B. SharingStarted.Eagerly, um den Producer sofort zu starten, oder SharingStarted.Lazily, um die Freigabe nach dem ersten Abonnenten zu starten und den Flow für immer aktiv zu halten.

SharedFlow

Die Funktion shareIn gibt einen SharedFlow zurück, einen Hot Flow, der Werte an alle Consumer ausgibt, die Daten daraus abrufen. Eine SharedFlow ist eine hochgradig konfigurierbare Verallgemeinerung von StateFlow.

Sie können ein SharedFlow erstellen, ohne shareIn zu verwenden. Sie könnten beispielsweise einen SharedFlow verwenden, um Ticks an den Rest der App zu senden, damit alle Inhalte regelmäßig gleichzeitig aktualisiert werden. Neben dem Abrufen der neuesten Nachrichten sollten Sie auch den Abschnitt mit Nutzerinformationen mit der Sammlung der Lieblingsthemen aktualisieren. Im folgenden Code-Snippet macht ein TickHandler ein SharedFlow verfügbar, damit andere Klassen wissen, wann sie ihren Inhalt aktualisieren müssen. Wie bei StateFlow verwenden Sie in einer Klasse eine Backing-Property vom Typ MutableSharedFlow, um Elemente an den Flow zu senden:

// Class that centralizes when the content of the app needs to be refreshed
class TickHandler(
    private val externalScope: CoroutineScope,
    private val tickIntervalMs: Long = 5000
) {
    // Backing property to avoid flow emissions from other classes
    private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
    val tickFlow: SharedFlow<Unit> = _tickFlow

    init {
        externalScope.launch {
            while (true) {
                _tickFlow.emit(Unit)
                delay(tickIntervalMs)
            }
        }
    }
}

class NewsRepository(
    // ...
    private val tickHandler: TickHandler,
    private val externalScope: CoroutineScope
) {
    init {
        externalScope.launch {
            // Listen for tick updates
            tickHandler.tickFlow.collect {
                refreshLatestNews()
            }
        }
    }

    suspend fun refreshLatestNews() { /* ... */ }
    // ...
}

Sie haben folgende Möglichkeiten, das Verhalten von SharedFlow anzupassen:

  • Mit replay können Sie eine Reihe von zuvor gesendeten Werten für neue Abonnenten noch einmal senden.
  • Mit onBufferOverflow können Sie eine Richtlinie für den Fall angeben, dass der Puffer mit zu sendenden Elementen gefüllt ist. Der Standardwert ist BufferOverflow.SUSPEND, wodurch der Anrufer pausiert wird. Weitere Optionen sind DROP_LATEST oder DROP_OLDEST.

MutableSharedFlow hat auch die Property subscriptionCount, die die Anzahl der aktiven Sammler enthält. So können Sie Ihre Geschäftslogik entsprechend optimieren. MutableSharedFlow enthält auch die Funktion resetReplayCache, wenn Sie die letzten an den Flow gesendeten Informationen nicht wiedergeben möchten.

Zusätzliche Ressourcen für Abläufe