StateFlow et SharedFlow

StateFlow et SharedFlow sont des API Flow qui permettent aux flux d'émettre de manière optimale des mises à jour d'état ainsi que des valeurs à plusieurs consommateurs.

StateFlow

StateFlow est un flux observable d'état qui émet les mises à jour d'état actuelles et nouvelles à ses collecteurs. La valeur d'état actuelle peut également être lue via sa propriété value. Pour mettre à jour l'état et l'envoyer au flux, attribuez une nouvelle valeur à la propriété value de la classe MutableStateFlow.

Dans Android, StateFlow convient parfaitement aux classes qui doivent conserver un état observable modifiable.

En suivant les exemples des flux Kotlin, un StateFlow peut être exposé à partir du LatestNewsViewModel afin que la View puisse écouter les mises à jour de l'état de l'interface utilisateur et faire en sorte que l'état de l'écran survive aux modifications de configuration.

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 View 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()
}

La classe responsable de la mise à jour d'un MutableStateFlow est le producteur, et toutes les classes effectuant la collecte à partir de StateFlow sont les consommateurs. Contrairement à un flux froid créé à l'aide du constructeur flow, un StateFlow est chaud : la collecte à partir du flux ne déclenche aucun code de producteur. Un StateFlow est toujours actif et en mémoire, et il ne devient éligible à la récupération de mémoire qu'à partir du moment où aucune autre référence n'y est faite.

Lorsqu'un nouveau consommateur commence la collecte à partir du flux, il reçoit le dernier état du flux et tous les états ultérieurs. Vous pouvez retrouver ce comportement dans d'autres classes observables telles que LiveData.

La View écoute StateFlow comme tout autre flux :

class LatestNewsActivity : AppCompatActivity() {
    private val latestNewsViewModel = // 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)
                    }
                }
            }
        }
    }
}

Pour convertir un flux en StateFlow, utilisez l'opérateur intermédiaire stateIn.

StateFlow, Flow et LiveData

StateFlow et LiveData présentent des similitudes. Ce sont toutes deux des classes de conteneurs de données observables, et elles suivent un modèle similaire lorsqu'elles sont utilisées dans l'architecture de votre application.

Notez toutefois que StateFlow et LiveData se comportent différemment :

  • StateFlow nécessite qu'un état initial soit transmis au constructeur, ce qui n'est pas le cas de LiveData.
  • LiveData.observe() désenregistre automatiquement le consommateur lorsque la vue passe à l'état STOPPED, tandis que la collecte à partir d'un StateFlow ou de tout autre flux n'arrête pas automatiquement la collecte. Pour obtenir le même comportement, vous devez collecter le flux à partir d'un bloc Lifecycle.repeatOnLifecycle.

Transformer les flux froids en flux chauds à l'aide de shareIn

StateFlow est un flux chaud qui reste en mémoire tant qu'il est collecté ou qu'il existe d'autres références à celui-ci à partir d'une racine de récupération de mémoire. Vous pouvez transformer des flux froids en flux chauds à l'aide de l'opérateur shareIn.

En prenant comme exemple le callbackFlow créé dans les flux Kotlin, au lieu de demander à chaque collecteur de créer un nouveau flux, vous pouvez partager les données extraites de Firestore entre les collecteurs en utilisant shareIn. Pour ce faire, vous devez transmettre les éléments suivants :

  • Un CoroutineScope utilisé pour partager le flux. Ce champ d'application doit durer plus longtemps que tout consommateur afin de maintenir le flux partagé en vie aussi longtemps que nécessaire.
  • Le nombre d'éléments à répéter à chaque nouveau collectionneur.
  • La règle du comportement de démarrage.
class NewsRemoteDataSource(...,
    private val externalScope: CoroutineScope,
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        ...
    }.shareIn(
        externalScope,
        replay = 1,
        started = SharingStarted.WhileSubscribed()
    )
}

Dans cet exemple, le flux latestNews répète le dernier élément émis dans un nouveau collecteur et reste actif tant que externalScope est actif et qu'il existe des collecteurs actifs. La règle de démarrage SharingStarted.WhileSubscribed() maintient le producteur en amont actif tant que des abonnés sont actifs. D'autres règles de démarrage sont disponibles, telles que SharingStarted.Eagerly pour démarrer le producteur immédiatement ou SharingStarted.Lazily pour commencer le partage après l'apparition du premier abonné et garder le flux actif indéfiniment.

SharedFlow

La fonction shareIn renvoie un SharedFlow, un flux chaud émettant des valeurs pour tous les consommateurs qui le collectent. Un SharedFlow est une généralisation hautement configurable de StateFlow.

Vous pouvez créer un SharedFlow sans utiliser shareIn. Par exemple, vous pouvez utiliser un SharedFlow pour envoyer des ticks au reste de l'application, afin que tout le contenu soit actualisé régulièrement en même temps. En plus de récupérer les dernières nouvelles, vous pouvez actualiser la section des informations sur l'utilisateur avec sa collection de sujets favoris. Dans l'extrait de code suivant, un TickHandler expose un SharedFlow afin que les autres classes sachent quand actualiser son contenu. Comme pour StateFlow, utilisez une propriété de support de type MutableSharedFlow dans une classe pour envoyer des éléments au flux :

// 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<Event<String>> = _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() { ... }
    ...
}

Vous pouvez personnaliser le comportement de SharedFlow de différentes façons :

  • replay vous permet de renvoyer un certain nombre de valeurs précédemment émises pour les nouveaux abonnés.
  • onBufferOverflow vous permet de spécifier une règle indiquant quand le tampon contient un grand nombre d'éléments à envoyer. La valeur par défaut est BufferOverflow.SUSPEND, ce qui entraîne la suspension de l'appelant. Les autres options sont DROP_LATEST ou DROP_OLDEST.

MutableSharedFlow comporte également une propriété subscriptionCount contenant le nombre de collecteurs actifs afin que vous puissiez optimiser la logique métier en conséquence. MutableSharedFlow contient également une fonction resetReplayCache si vous ne souhaitez pas répéter les dernières informations envoyées au flux.

Ressources supplémentaires sur les flux