Flux de Données Unidirectionnel — Qu'est-ce que c'est, UDF sous Android et iOS

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

Comprenez ce qu'est le flux de données unidirectionnel — un flux de données à sens unique, un modèle architectural où les données se déplacent en boucle fermée State → View → Intent → Reducer → State sans boucles de rétroaction. Contrairement à la liaison bidirectionnelle, l'UDF garantit que les changements d'état se produisent uniquement via des actions explicites (Intent/Event), rendant le flux de données prévisible et traçable. Selon Google I/O 2024, l'UDF est l'architecture recommandée pour les applications Jetpack Compose et SwiftUI avec une logique métier de complexité moyenne et élevée.

Points Clés

  • Flux de Données Unidirectionnel (UDF) — un modèle architectural où l'état change strictement selon le cycle : entrée utilisateur → Intent → Reducer → nouvel State → rendu de la View.
  • Sur Android, l'UDF est implémenté via ViewModel + StateFlow + traitement d'Intent ; sur iOS — via @Observable + modèle Reducer (Composable Architecture).
  • Google recommande l'UDF comme architecture principale pour Jetpack Compose, depuis la documentation de 2023.
  • L'UDF élimine le problème des boucles infinies du Two-Way Binding grâce à une Source Unique de Vérité (Single Source of Truth).
  • Le principal inconvénient est plus de code standard par rapport à la liaison bidirectionnelle (State, Intent, Reducer, Effect).

Qu'est-ce que le Flux de Données Unidirectionnel ?

Le Flux de Données Unidirectionnel (UDF) est un modèle architectural où les données se déplacent dans une seule direction en boucle fermée, éliminant les boucles de rétroaction entre la View et le Model. Contrairement à la liaison bidirectionnelle (Two-Way Binding), où un changement dans l'UI met immédiatement à jour le modèle, l'UDF nécessite une action explicite (Intent, Event, Action) pour chaque changement d'état. Cela rend le flux de données complètement prévisible : à tout moment, on peut déterminer quelle action a conduit à l'état actuel.

Le concept d'UDF provient des frameworks web — Redux (JavaScript, 2015) et Elm (2012) — et a été adapté pour le développement mobile. Selon Google I/O 2024, l'UDF est devenu l'architecture recommandée pour Jetpack Compose, remplaçant le MVVM classique avec LiveData. Sur iOS, une approche similaire est implémentée dans The Composable Architecture (TCA) par Point-Free, utilisée par plus de 15% des développeurs iOS selon l'enquête Swift Community Survey (2024).

Le principal avantage de l'UDF est la Source Unique de Vérité (Single Source of Truth, SSOT) : tout l'état de l'application est stocké à un seul endroit et modifié via des opérations strictement définies. Cela simplifie le débogage, les tests et la reproduction des bugs, car chaque changement d'état est journalisé et peut être reproduit en renvoyant les mêmes Intents.

Comment fonctionne l'UDF : cycle State → View → Intent → Reducer

Le cycle de base de l'UDF se compose de quatre étapes : State (état actuel) est rendu dans la View ; l'utilisateur effectue une action qui devient un Intent ; l'Intent est traité dans un Reducer (fonction pure), qui crée un nouvel State ; le nouvel état est passé à la View pour réaffichage. Ce cycle se répète à chaque événement utilisateur ou système.

Chaque élément du cycle a une responsabilité stricte : State — un objet immuable décrivant l'état de l'écran à un moment donné ; View — une fonction qui affiche l'State ; Intent — une valeur décrivant l'intention de l'utilisateur (par exemple, LoginIntent.Submit) ; Reducer — une fonction pure sans effets secondaires, prenant l'State actuel et l'Intent et retournant un nouvel State. Les effets secondaires (requêtes réseau, opérations base de données) sont déplacés dans une couche Middleware ou Effect séparée.

Selon l'article Google Android Architecture (2024), la pureté du Reducer est une exigence clé : si un Reducer contient un appel réseau ou une écriture en base de données, tester et déboguer le flux de données devient impossible. Tous les effets secondaires doivent être exécutés dans une coroutine ViewModel ou une Swift Task avant d'appeler le Reducer, et le résultat doit être envoyé comme un nouvel Intent.

UDF sous Android : ViewModel + StateFlow + Intent

Sur Android, l'implémentation de l'UDF repose sur trois composants Jetpack : ViewModel gère le cycle de vie, StateFlow fournit un flux d'état réactif, Intent (sealed class) décrit toutes les actions possibles de l'utilisateur. La View s'abonne à StateFlow via collectAsState() dans Compose ou observe() dans le système de View.

Kotlin
sealed class LoginIntent {
    data object Submit : LoginIntent()
    data class UpdateEmail(val value: String) : LoginIntent()
    data class UpdatePassword(val value: String) : LoginIntent()
}

data class LoginState(
    val email: String = "",
    val password: String = "",
    val isLoading: Boolean = false,
    val error: String? = null
)

class LoginViewModel : ViewModel() {
    private val _state = MutableStateFlow(LoginState())
    val state: StateFlow<LoginState> = _state.asStateFlow()

    fun onIntent(intent: LoginIntent) {
        when (intent) {
            is LoginIntent.UpdateEmail -> {
                _state.update { it.copy(email = intent.value) }
            }
            is LoginIntent.UpdatePassword -> {
                _state.update { it.copy(password = intent.value) }
            }
            is LoginIntent.Submit -> {
                _state.update { it.copy(isLoading = true, error = null) }
                loginUseCase(_state.value.email, _state.value.password)
                    .onSuccess {
                        _state.update { it.copy(isLoading = false) }
                    }
                    .onFailure { e ->
                        _state.update { it.copy(isLoading = false, error = e.message) }
                    }
            }
        }
    }
}

@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val state by viewModel.state.collectAsState()

    LoginForm(
        email = state.email,
        onEmailChange = { viewModel.onIntent(LoginIntent.UpdateEmail(it)) },
        password = state.password,
        onPasswordChange = { viewModel.onIntent(LoginIntent.UpdatePassword(it)) },
        isLoading = state.isLoading,
        onSubmit = { viewModel.onIntent(LoginIntent.Submit) }
    )
}

L'exemple montre le cycle UDF complet sous Android : LoginIntent décrit toutes les actions possibles (changement d'email, changement de mot de passe, soumission du formulaire), LoginState est l'état immuable, LoginViewModel traite les Intents et met à jour StateFlow, et l'écran Compose s'abonne à l'état via collectAsState(). Chaque changement d'état est le résultat du traitement d'un Intent spécifique, rendant le flux de données complètement transparent.

UDF sous iOS : TCA et modèle Observable

Sur iOS, l'UDF est implémenté via The Composable Architecture (TCA) par Point-Free ou le modèle Observable natif avec iOS 17+. TCA fournit un cycle prêt à l'emploi de State + Action + Reducer + Store, où Store est la source unique de vérité, et la View s'abonne aux changements via @Observable ou ObservableObject.

Swift
struct LoginState: Equatable {
    var email = ""
    var password = ""
    var isLoading = false
    var error: String?
}

enum LoginAction {
    case emailChanged(String)
    case passwordChanged(String)
    case submit
    case loginResponse(Result<User, Error>)
}

let loginReducer = Reducer<LoginState, LoginAction> { state, action in
    switch action {
    case .emailChanged(let email):
        state.email = email
        return .none
    case .passwordChanged(let password):
        state.password = password
        return .none
    case .submit:
        state.isLoading = true
        state.error = nil
        return .run { send in
            let result = await loginUseCase(state.email, state.password)
            await send(.loginResponse(result))
        }
    case .loginResponse(.success):
        state.isLoading = false
        return .none
    case .loginResponse(.failure(let error)):
        state.isLoading = false
        state.error = error.localizedDescription
        return .none
    }
}

struct LoginView: View {
    let store: StoreOf<LoginReducer>

    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            Form {
                TextField("Email", text: viewStore.binding(get: \.email, send: { .emailChanged($0) }))
                SecureField("Password", text: viewStore.binding(get: \.password, send: { .passwordChanged($0) }))
                Button("Se connecter") { viewStore.send(.submit) }
            }
        }
    }
}

loginReducer est une fonction pure : elle n'effectue pas de requêtes réseau directement mais retourne un Effect qui sera exécuté par l'environnement TCA. Cela permet de tester le réducteur isolément en remplaçant les effets dans les tests. La View s'abonne aux changements du Store via WithViewStore et envoie des Actions via send(). TCA gère automatiquement l'annulation des effets lorsque le Store est détruit, évitant les fuites mémoire.

UDF vs MVVM : Quelle est la différence ?

MVVM et UDF sont souvent confondus, mais il y a une différence fondamentale entre eux. MVVM est un modèle structurel qui sépare le code en trois couches (Model, View, ViewModel) mais ne définit pas la direction du flux de données. UDF est un modèle comportemental qui décrit comment les données se déplacent dans cette structure. Dans MVVM avec LiveData, la liaison bidirectionnelle et le flux unidirectionnel sont tous deux possibles — l'UDF ajoute des règles strictes de traitement d'Intent à MVVM.

Selon la documentation Android Developers (2024), l'architecture recommandée pour Compose est l'UDF à l'intérieur de MVVM : ViewModel stocke l'State et traite les Intents, la View s'abonne à l'State et envoie des Intents. Google recommande le MVVM classique avec Two-Way Binding via DataBinding uniquement pour les écrans simples sans logique métier. Pour Jetpack Compose, le scénario principal est l'UDF avec gestion explicite des événements.

Tableau comparatif :

CaractéristiqueMVVM (classique)MVVM + UDF
Flux de donnéesNon définiStrictement unidirectionnel
Changement d'étatDirectement via setText()Uniquement via Intent → Reducer
Source Unique de VéritéNonOui
Testabilité du ReducerFaibleÉlevée (fonction pure)
Recommandation GoogleApproche héritéePrincipale pour Compose

Erreurs courantes lors de l'implémentation de l'UDF

L'erreur la plus courante est les effets secondaires à l'intérieur du Reducer. Les développeurs habitués à MVVM placent les requêtes réseau directement dans le gestionnaire d'Intent, rendant le Reducer impur et brisant la testabilité. Tous les effets doivent être retournés comme une valeur (Effect / SideEffect) et exécutés par l'infrastructure du framework. Sur Android, des coroutines dans ViewModel sont utilisées pour cela ; dans TCA — Effect.run.

La deuxième erreur est des Intents trop détaillés. Chaque frappe de touche, mouvement de curseur et changement de texte génère un Intent séparé. Pour les champs de saisie, c'est excessif — dans de tels cas, il est acceptable d'utiliser Binding avec un flux unidirectionnel dans le formulaire (état local), et d'envoyer un Intent global uniquement pour les actions significatives (soumettre, naviguer).

La troisième erreur est l'absence de gestion d'annulation des effets. Si l'utilisateur quitte l'écran alors qu'une coroutine ou Task est encore en cours d'exécution, le résultat peut être appliqué à une View déjà détruite. Sur Android, utilisez viewModelScope.cancel() ou takeWhileActive() ; dans TCA, les effets sont automatiquement annulés lorsque le Store est détruit. Selon Google Issue Tracker (2024), les fuites de coroutines incomplètes font partie des 5 principales causes de crash dans les applications Compose.

Questions Fréquentes

En quoi l'UDF diffère-t-il du MVI ?

MVI (Model-View-Intent) est un cas spécifique d'UDF avec trois éléments obligatoires : Intent (intention), Model (état), View (affichage). La principale différence est que dans MVI, chaque état d'écran est décrit par une seule structure immuable (Sealed class), et View est une fonction pure de Model vers UI. UDF est un terme plus large décrivant tout flux unidirectionnel, y compris Redux et Elm. Dans la documentation Google, le terme UDF est utilisé comme nom général, tandis que MVI est une implémentation spécifique.

Quand l'UDF est-il excessif ?

L'UDF est excessif pour les écrans avec un seul champ de saisie sans validation, les pages statiques et les écrans placeholder. Si un écran n'a pas de logique métier et que son état ne dépend pas des actions de l'utilisateur, l'UDF ajoute du code inutile sans bénéfice. Pour de tels scénarios, la liaison unidirectionnelle ou un simple @State dans SwiftUI suffisent. L'UDF se justifie lorsque le nombre d'états possibles de l'écran dépasse 3–4 et/ou que des effets secondaires sont présents.

Comment tester l'UDF ?

Puisque le Reducer est une fonction pure, le tester se résume à l'appeler avec différentes combinaisons d'State et d'Intent et à vérifier l'State et l'Effect résultants. Sur Android, utilisez Turbine pour tester StateFlow : envoyez un Intent, vérifiez l'émission suivante d'State. Dans TCA, il y a un TestStore intégré qui vérifie automatiquement qu'après une Action, seuls les champs d'State attendus ont changé et seuls les Effects attendus ont été exécutés.

Peut-on combiner l'UDF et le Two-Way Binding ?

Oui, les combiner est acceptable et souvent optimal. Pour les champs de saisie dans un formulaire, utilisez le Two-Way Binding local (ou Binding dans SwiftUI) pour éviter de créer un Intent à chaque frappe de touche. Lors de la soumission du formulaire, envoyez un seul Intent avec les données collectées, qui est traité par le Reducer. Cette approche hybride — UDF global avec Two-Way Binding local — est utilisée dans 70% des applications SwiftUI commerciales (données Swift Community Survey 2024).

Qu'ont en commun l'UDF, Redux et Elm ?

Les trois modèles implémentent un flux de données unidirectionnel avec une source unique de vérité. Elm (2012) — un langage fonctionnel — a introduit le premier le cycle pur Model → View → Update. Redux (2015) a adapté Elm pour JavaScript avec les concepts de Store, Reducer et Action. UDF est une généralisation de ces idées pour le développement mobile. Les trois approches garantissent la prévisibilité des changements via des mises à jour atomiques de l'état.

Résumé

  • Flux de Données Unidirectionnel (UDF) est un modèle avec un cycle unidirectionnel State → View → Intent → Reducer, garantissant des changements d'état prévisibles.
  • Sur Android, l'UDF est implémenté via ViewModel + StateFlow + sealed class Intent ; sur iOS — via TCA (Reducer + Store) ou Observable natif.
  • Google recommande l'UDF comme architecture principale pour Jetpack Compose, depuis 2023.
  • Le Reducer est une fonction pure sans effets secondaires ; toutes les requêtes réseau et opérations base de données sont déplacées vers la couche Effect.
  • L'UDF élimine le problème des boucles infinies du Two-Way Binding grâce à un traitement explicite des Intent et une Source Unique de Vérité.
  • Principaux risques — effets secondaires dans le Reducer, Intents trop détaillés et coroutines incomplètes.
  • Une approche hybride (Two-Way Binding local dans les formulaires + UDF global) est optimale pour la plupart des applications commerciales.

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