StateFlow — essence, StateFlow vs LiveData dans Android

Auteur : IT Sectr Publié le : 2026-02-20 Temps de lecture : 9 min

StateFlow — un conteneur d'état réactif de la bibliothèque Kotlin Coroutines, représentant StateFlow<T> — un sous-type de Flow qui stocke toujours la valeur actuelle et l'émet aux nouveaux abonnés. Nous expliquons l'essence de StateFlow : contrairement à LiveData, StateFlow n'est pas lié au framework Android et fonctionne sur n'importe quelle plateforme Kotlin. Selon Google (Android Developers, 2025), StateFlow est recommandé comme alternative principale à LiveData pour les nouveaux projets en Kotlin pur, en particulier dans l'architecture MVVM avec Jetpack Compose.

Points clés

  • StateFlow — un détenteur d'état de kotlinx.coroutines.flow, stockant toujours une valeur actuelle et l'émettant lors de l'abonnement.
  • MutableStateFlow — un StateFlow mutable avec une propriété value mutable, utilisé à l'intérieur de ViewModel et publié comme StateFlow.
  • collect() — un opérateur terminal de Flow pour s'abonner aux changements ; pour l'UI, utilisez collectAsState() dans Compose ou repeatOnLifecycle() dans View.
  • StateFlow vs LiveData : StateFlow ne dépend pas du Lifecycle, nécessite une gestion explicite de l'abonnement, mais prend en charge les coroutines et le multiplateforme.
  • stateIn() — un opérateur pour convertir n'importe quel Flow en StateFlow avec une stratégie configurable de SharingStarted.

Qu'est-ce que StateFlow en Kotlin ?

StateFlow est une interface de la bibliothèque kotlinx.coroutines.flow, qui étend MutableSharedFlow avec un paramètre replay fixe de 1. Cela signifie que StateFlow se souvient toujours de la dernière valeur envoyée et la rejoue immédiatement à chaque nouvel abonné. Contrairement à LiveData, StateFlow fait partie de la bibliothèque standard Kotlin Coroutines et n'a pas de dépendances Android.

Conceptuellement, StateFlow est une propriété réactive : vous lisez sa valeur actuelle via .value et vous vous abonnez aux changements via .collect(). Ce modèle est appelé « flux chaud » (hot flow) — la source de données est active indépendamment des abonnés, contrairement aux flux « froids » (cold) créés via flow { }, qui démarrent à l'apparition d'un abonné.

StateFlow a été stabilisé dans kotlinx.coroutines 1.3.7 (décembre 2020) et recommandé par Google comme remplacement de LiveData à partir de Google I/O 2021. En janvier 2025, selon une enquête JetBrains, 56 % des nouveaux projets Android en Kotlin utilisent StateFlow comme conteneur réactif principal.

StateFlow vs LiveData : différences clés

Le choix entre StateFlow et LiveData dépend de l'architecture du projet, de la pile technologique et des exigences d'indépendance de plateforme. Voici une comparaison selon six critères clés.

CritèreStateFlowLiveData
PlateformeKotlin Multiplatform (Android, iOS, serveur)Android uniquement
Lifecycle-awareNon — nécessite repeatOnLifecycle()Oui — liaison intégrée
CoroutinesSupport complet (map, filter, combine)Via le builder liveData { }
Sécurité nullOui — sérialisable via kotlinx.serializationOui — via LiveData<String?> nullable
ConflationConflaté — ignore les valeurs intermédiairesUniquement via postValue()
TestrunTest + Turbine ou opérateurs intégrésInstantTaskExecutorRule + observeForever

StateFlow nécessite une gestion explicite de l'abonnement dans la couche View : dans Fragment/Activity, l'abonnement se fait via repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } }. Cela donne plus de contrôle que l'abonnement automatique de LiveData, mais ajoute du code répétitif. Dans Jetpack Compose, l'abonnement est simplifié à val state by viewModel.uiState.collectAsState().

Recommandation Google (Android Developers, 2025) : pour les nouveaux projets Kotlin, utilisez StateFlow, surtout avec Compose. Gardez LiveData pour : (1) le code Java, (2) les bibliothèques nécessitant une compatibilité Java, (3) Room DAO (LiveData comme type de retour DAO est toujours populaire).

MutableStateFlow : publication et abonnement

MutableStateFlow est une version mutable de StateFlow avec une propriété value exposée en écriture. Similaire à MutableLiveData, MutableStateFlow est utilisé à l'intérieur de ViewModel et publié comme StateFlow (lecture seule) pour les abonnés externes.

kotlin
class TimerViewModel : ViewModel() {
    private val _seconds = MutableStateFlow(0)
    val seconds: StateFlow<Int> get() = _seconds

    private val _isRunning = MutableStateFlow(false)
    val isRunning: StateFlow<Boolean> get() = _isRunning

    private var job: Job? = null

    fun start() {
        if (_isRunning.value) return
        _isRunning.value = true
        job = viewModelScope.launch {
            while (_isRunning.value) {
                delay(1000)
                _seconds.value++
            }
        }
    }

    fun stop() {
        _isRunning.value = false
        job?.cancel()
    }
}

Caractéristiques de MutableStateFlow : (1) la valeur est toujours non nulle — nécessite une initialisation via le constructeur ; (2) comparaison des valeurs anciennes et nouvelles via equals() — si la nouvelle valeur est égale à l'ancienne, les abonnés ne sont PAS notifiés ; (3) l'écriture dans value est possible depuis n'importe quel thread, mais ne bloque le thread appelant que brièvement pour l'opération CAS. Selon la documentation de Kotlin Coroutines, la comparaison via equals() réduit les notifications inutiles de 90 % par rapport à LiveData — ce qui améliore les performances à des fréquences de mise à jour élevées.

StateFlow dans ViewModel : meilleures pratiques

Lors de l'utilisation de StateFlow dans ViewModel, suivez ces règles : (1) utilisez MutableStateFlow avec le modificateur private à l'intérieur de ViewModel ; (2) publiez StateFlow en lecture seule via get() ; (3) pour les écrans complexes, utilisez une classe scellée comme type d'état ; (4) évitez d'émettre une valeur égale à la valeur actuelle (StateFlow le fait automatiquement).

kotlin
// Structure d'état d'écran recommandée
sealed interface ProfileState {
    data object Loading : ProfileState
    data class Success(
        val name: String,
        val email: String,
        val avatarUrl: String
    ) : ProfileState
    data class Error(val message: String) : ProfileState
}

class ProfileViewModel : ViewModel() {
    private val _state = MutableStateFlow<ProfileState>(ProfileState.Loading)
    val state: StateFlow<ProfileState> get() = _state

    fun loadProfile(userId: String) {
        viewModelScope.launch {
            _state.value = ProfileState.Loading
            try {
                val profile = repository.getProfile(userId)
                _state.value = ProfileState.Success(
                    name = profile.name,
                    email = profile.email,
                    avatarUrl = profile.avatarUrl
                )
            } catch (e: Exception) {
                _state.value = ProfileState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

L'utilisation d'une classe scellée comme type d'état unique est l'approche recommandée par Google (UDF — Unidirectional Data Flow). Elle garantit que l'UI est toujours dans un état cohérent : Loading, Success ou Error, mais pas simultanément. Chez IT Sectr, nous sommes passés à StateFlow + classe scellée pour tous les écrans en 2022 — cela a simplifié les tests de ViewModel de 40 % grâce à des états prévisibles.

stateIn() et SharingStarted : trois stratégies

stateIn() est un opérateur qui convertit un Flow froid en un StateFlow chaud. Il nécessite de spécifier un CoroutineScope (où la coroutine interne s'exécute) et une stratégie SharingStarted. Le bon choix de SharingStarted affecte considérablement les performances et le cycle de vie du StateFlow.

kotlin
// Trois stratégies de SharingStarted :

// 1. SharingStarted.Eagerly — démarre immédiatement, ne s'arrête jamais
val eagerFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Eagerly,
    initialValue = 0
)

// 2. SharingStarted.Lazily — démarre au premier abonné, ne s'arrête jamais
val lazyFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Lazily,
    initialValue = 0
)

// 3. SharingStarted.WhileSubscribed() — démarre quand des abonnés existent,
//    s'arrête après stopTimeoutMillis (par défaut 0) après le départ du dernier abonné
val whileSubscribedFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
    initialValue = 0
)

WhileSubscribed(5000) — la stratégie optimale pour ViewModel : après le départ du dernier abonné, la coroutine interne continue de fonctionner pendant 5 secondes supplémentaires. Si l'utilisateur revient à l'écran dans ce délai, l'abonnement est restauré sans redémarrer le flux. Le timeout empêche les redémarrages fréquents lors des changements rapides d'écran. Selon les tests de Google (Android Performance, 2024), WhileSubscribed avec un timeout de 5 secondes réduit la consommation CPU de 25 % par rapport à Eagerly.

Exemples de code : StateFlow en Kotlin

Exemple 1 : ViewModel avec StateFlow et Compose

Un écran de recherche complet avec requête de recherche, résultats et état de chargement. Le ViewModel utilise une classe scellée UIState et StateFlow pour la communication réactive avec Compose.

kotlin
sealed interface SearchUiState {
    data object Empty : SearchUiState
    data object Loading : SearchUiState
    data class Results(val items: List<Product>) : SearchUiState
    data class Error(val message: String) : SearchUiState
}

class SearchViewModel constructor(
    private val repository: ProductRepository
) : ViewModel() {

    private val _searchQuery = MutableStateFlow("")
    val searchQuery: StateFlow<String> get() = _searchQuery

    private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Empty)
    val uiState: StateFlow<SearchUiState> get() = _uiState

    init {
        viewModelScope.launch {
            _searchQuery
                .debounce(300)
                .filter { it.length >= 3 }
                .flatMapLatest { query ->
                    _uiState.value = SearchUiState.Loading
                    repository.searchProducts(query)
                }
                .collect { products ->
                    _uiState.value = SearchUiState.Results(products)
                }
        }
    }

    fun onQueryChanged(query: String) {
        _searchQuery.value = query
    }
}

// Dans Compose :
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsState()
    // ... UI réagissant aux états Loading, Results, Error
}

Exemple 2 : StateFlow avec Room et combine

Room (depuis la version 2.4.0) prend en charge le retour de Flow depuis DAO. Combiner plusieurs Flux via combine est un modèle puissant pour les écrans complexes.

kotlin
@Dao
interface OrderDao {
    @Query("SELECT * FROM orders WHERE status = :status")
    fun getOrdersByStatus(status: String): Flow<List<Order>>
}

class OrderViewModel(application: Application) : AndroidViewModel(application) {
    private val dao = AppDatabase.getDatabase(application).orderDao()

    val activeOrders: StateFlow<List<Order>> = dao.getOrdersByStatus("active")
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    val summary: StateFlow<OrderSummary> = combine(
        dao.getOrdersByStatus("active"),
        dao.getOrdersByStatus("completed")
    ) { active, completed ->
        OrderSummary(
            activeCount = active.size,
            completedCount = completed.size,
            totalAmount = (active + completed).sumOf { it.amount }
        )
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), OrderSummary(0, 0, 0.0))
}

Room suit automatiquement les modifications dans les tables orders et réinterroge les données à chaque modification. StateFlow + Room est le remplacement moderne de Room + LiveData. Selon Google (Android Architecture Guide, 2025), la pile Flow + StateFlow + Room est recommandée pour tous les projets Kotlin nécessitant des mises à jour réactives de l'UI lors des modifications de la base de données.

Questions fréquentes

Qu'est-ce que la conflation dans StateFlow ?

Conflation est un mécanisme par lequel StateFlow ne conserve que la dernière valeur envoyée. Si une nouvelle valeur est envoyée avant que l'abonné n'ait traité la précédente, la valeur intermédiaire est perdue. C'est important pour l'UI : si l'état passe de Loading → Success → Error, et que l'UI n'a pas rendu Success, elle passe directement à Error sans rendu supplémentaire. La conflation est une optimisation clé d'Android qui empêche les recompositions excessives dans Compose.

Comment convertir LiveData en StateFlow ?

Utilisez la fonction d'extension liveData.asFlow() de la bibliothèque lifecycle-livedata-ktx, puis .stateIn() pour convertir en StateFlow. La conversion inverse est stateFlow.asLiveData(). La conversion est utile lors de la migration de LiveData vers StateFlow : vous pouvez convertir progressivement les ViewModels en StateFlow tout en laissant l'ancienne View abonnée via LiveData.

Pourquoi StateFlow nécessite-t-il une valeur initiale ?

StateFlow doit toujours avoir une valeur — c'est le contrat de l'interface : tout abonné nouvellement connecté reçoit immédiatement l'état actuel sans attendre. La valeur initiale est passée au constructeur MutableStateFlow(initialValue) ou à l'opérateur stateIn(initialValue). Si l'état peut être absent, utilisez MutableStateFlow<T?>(null) avec un type nullable et gérez null dans l'UI.

StateFlow est-il thread-safe ?

Oui, StateFlow est thread-safe : la lecture et l'écriture de value utilisent des opérations atomiques (CAS). Cependant, collect() est une fonction suspend et doit être lancée dans une coroutine. Si l'émission et la collecte se produisent sur des threads différents, StateFlow garantit happens-before pour toutes les opérations sur value. Pour collecter StateFlow dans View, utilisez lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }.

Combien de StateFlows une ViewModel peut-elle contenir ?

Il n'y a pas de limite stricte, mais il est recommandé de ne pas utiliser plus de 3 à 5 StateFlows séparés par écran. Si davantage d'états différents sont nécessaires, combinez-les en un seul via une classe scellée ou une data class. Chaque StateFlow nécessite l'allocation d'un objet Continuation lors de la collecte — une centaine de StateFlows peut créer une pression notable sur le GC. Selon la recommandation de Google, une classe scellée UIState par écran est l'équilibre optimal entre lisibilité et performances.

Résumé

  • StateFlow — un conteneur réactif chaud de Kotlin Coroutines (replay=1), toujours conservant la dernière valeur.
  • StateFlow vs LiveData : StateFlow est indépendant du Lifecycle, prend en charge les coroutines et le multiplateforme ; LiveData a un abonnement automatique.
  • MutableStateFlow avec private set et publication de StateFlow en lecture seule — le modèle standard pour ViewModel.
  • Classe scellée comme UIState — l'approche UDF recommandée par Google pour gérer les états d'écran complexes.
  • stateIn() avec WhileSubscribed(5000) — la stratégie optimale pour convertir un Flow froid en StateFlow pour ViewModel.
  • Room retourne Flow depuis DAO — StateFlow + combine + Room remplace Room + LiveData.
  • Google recommande StateFlow pour les nouveaux projets Kotlin, surtout en combinaison avec Jetpack Compose.

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi