Unidirectional Data Flow — ce este, UDF în Android și iOS

Autor: IT Sectr Publicat: 2026-02-20 Timp de citire: 13 min

Înțelegeți ce este Unidirectional Data Flow — fluxul de date unidirecțional, un model arhitectural în care datele se deplasează într-un ciclu închis State → View → Intent → Reducer → State fără feedback. Spre deosebire de legarea bidirecțională, UDF garantează că modificarea stării are loc numai prin acțiuni explicite (Intent/Event), ceea ce face fluxul de date predictibil și trasabil. Conform Google I/O 2024, UDF este arhitectura recomandată pentru aplicațiile Jetpack Compose și SwiftUI cu logică de afaceri medie și înaltă.

Principalele

  • Unidirectional Data Flow (UDF) — model arhitectural în care starea se modifică strict după ciclul: intrarea utilizatorului → Intent → Reducer → State nou → redesenarea View.
  • În Android UDF se implementează prin ViewModel + StateFlow + procesarea Intent; în iOS — prin @Observable + modelul Reducer (Composable Architecture).
  • Google recomandă UDF ca arhitectură principală pentru Jetpack Compose, începând cu documentația din 2023.
  • UDF elimină problema buclelor infinite Two-Way Binding prin sursa unică de adevăr (Single Source of Truth).
  • Principalul dezavantaj — mai mult cod șablon comparativ cu legarea bidirecțională (State, Intent, Reducer, Effect).

Ce este Unidirectional Data Flow?

Unidirectional Data Flow (UDF) — un model arhitectural în care datele se deplasează într-o singură direcție de-a lungul unui ciclu închis, excluzând feedback-ul între View și Model. Spre deosebire de Two-Way Binding, unde o modificare în UI actualizează imediat modelul, UDF necesită o acțiune explicită (Intent, Event, Action) pentru fiecare modificare a stării. Acest lucru face fluxul de date complet predictibil: în orice moment se poate determina ce acțiune a dus la starea curentă.

Conceptul UDF provine din framework-urile web — Redux (JavaScript, 2015) și Elm (2012) — și a fost adaptat pentru dezvoltarea mobilă. Conform Google I/O 2024, UDF a devenit arhitectura recomandată pentru Jetpack Compose, înlocuind MVVM-ul clasic cu LiveData. Pe iOS o abordare similară este implementată în The Composable Architecture (TCA) de la Point-Free, pe care o folosesc peste 15% dintre dezvoltatorii iOS, conform sondajului Swift Community (2024).

Principalul avantaj al UDF — Single Source of Truth (SSOT): întreaga stare a aplicației este stocată într-un singur loc și modificată prin operații strict definite. Acest lucru simplifică depanarea, testarea și reproducerea bug-urilor, deoarece fiecare modificare a stării este înregistrată și poate fi reprodusă prin retrimiterea acelorași Intent.

Cum funcționează UDF: ciclul State → View → Intent → Reducer

Ciclul de bază UDF constă din patru pași: State (starea curentă) este afișată în View; utilizatorul efectuează o acțiune care se transformă în Intent (intenție); Intent este procesat în Reducer (o funcție pură), care creează o nouă State; noua stare este transmisă către View pentru redesenare. Acest ciclu se repetă la fiecare eveniment al utilizatorului sau de sistem.

Fiecare element al ciclului are o responsabilitate strictă: State — un obiect imutabil (immutable) care descrie starea ecranului la un moment dat; View — o funcție care afișează State; Intent — o valoare care descrie intenția utilizatorului (de exemplu, LoginIntent.Submit); Reducer — o funcție pură fără efecte secundare, care primește State și Intent curente și returnează o nouă State. Efectele secundare (cereri de rețea, baze de date) sunt mutate într-un strat separat Middleware sau Effect.

Conform articolului Google Android Architecture (2024), puritatea Reducer este o cerință cheie: dacă Reducer conține un apel de rețea sau o scriere în baza de date, testarea și depanarea fluxului de date devin imposibile. Toate efectele secundare trebuie executate în corutina ViewModel sau Swift Task înainte de apelarea Reducer, iar rezultatul trimis ca un nou Intent.

UDF în Android: ViewModel + StateFlow + Intent

În Android implementarea UDF se bazează pe trei componente Jetpack: ViewModel gestionează ciclul de viață, StateFlow asigură un flux reactiv de stări, Intent (sealed class) descrie toate acțiunile posibile ale utilizatorului. View se abonează la StateFlow prin collectAsState() în Compose sau observe() în sistemul 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) }
    )
}

Exemplul demonstrează ciclul complet UDF în Android: LoginIntent descrie toate acțiunile posibile (schimbarea emailului, parolei, trimiterea formularului), LoginState — starea imutabilă, LoginViewModel procesează Intent și actualizează StateFlow, iar ecranul Compose se abonează la state prin collectAsState(). Fiecare modificare a stării este rezultatul procesării unui Intent specific, ceea ce face fluxul de date complet transparent.

UDF în iOS: TCA și modelul Observable

Pe iOS UDF se implementează prin The Composable Architecture (TCA) de la Point-Free sau modelul Observable nativ cu iOS 17+. TCA oferă un ciclu gata State + Action + Reducer + Store, unde Store este singura sursă de adevăr, iar View se abonează la modificări prin @Observable sau 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("Conectare") { viewStore.send(.submit) }
            }
        }
    }
}

Reductorul loginReducer — o funcție pură: nu execută cereri de rețea direct, ci returnează un Effect care va fi executat de mediul TCA. Acest lucru permite testarea reductorului în izolare, înlocuind efectele în teste. View prin WithViewStore se abonează la modificările Store și trimite Action prin send(). TCA gestionează automat anularea efectelor la distrugerea Store, prevenind scurgerile de memorie.

UDF versus MVVM: care este diferența?

MVVM și UDF sunt adesea confundate, dar între ele există o diferență fundamentală. MVVM este un model structural care împarte codul în trei straturi (Model, View, ViewModel), dar nu definește direcția fluxului de date. UDF este un model comportamental care descrie cum se deplasează datele în interiorul acestei structuri. În MVVM cu LiveData poate exista atât legare bidirecțională, cât și flux unidirecțional — UDF adaugă la MVVM reguli stricte de procesare a Intent.

Conform documentației Android Developers (2024), arhitectura recomandată pentru Compose este UDF în interiorul MVVM: ViewModel stochează State și procesează Intent, View se abonează la State și trimite Intent. MVVM-ul clasic cu Two-Way Binding prin DataBinding este recomandat de Google doar pentru ecrane simple fără logică de afaceri. Pentru Jetpack Compose scenariul principal este UDF cu procesare explicită a evenimentelor.

Tabel comparativ:

CaracteristicăMVVM (clasic)MVVM + UDF
Fluxul de dateNedefinitStrict unidirecțional
Modificarea stăriiDirect prin setText()Numai prin Intent → Reducer
Single Source of TruthNuDa
Testabilitatea reductoruluiScăzutăRidicată (funcție pură)
Recomandarea GoogleAbordare învechităPrincipală pentru Compose

Greșeli tipice la implementarea UDF

Cea mai frecventă greșeală — efecte secundare în interiorul Reducer. Dezvoltatorii obișnuiți cu MVVM plasează cererile de rețea direct în handler-ul Intent, ceea ce face Reducer o funcție impură și strică testabilitatea. Toate efectele trebuie returnate ca valoare (Effect / SideEffect) și executate de infrastructura framework-ului. În Android se folosesc corutine în ViewModel, în TCA — Effect.run.

A doua greșeală — Intent prea detaliate. Fiecare apăsare de tastă, mișcare de slider și modificare de text generează un Intent separat. Pentru câmpurile de intrare acest lucru este excesiv — în astfel de cazuri este acceptabilă utilizarea Binding cu flux unidirecțional în interiorul formularului (stare locală), iar Intent global se trimite doar la acțiuni semnificative (submit, navigare).

A treia greșeală — lipsa gestionării anulării efectelor. Dacă utilizatorul părăsește ecranul, iar corutina sau Task continuă execuția, rezultatul poate fi aplicat unui View deja distrus. În Android utilizați viewModelScope.cancel() sau takeWhileActive(); în TCA efectele sunt anulate automat la distrugerea Store. Conform Google Issue Tracker (2024), scurgerile din corutine neterminate se află în top-5 cauze de crash ale aplicațiilor Compose.

Întrebări frecvente

Prin ce diferă UDF de MVI?

MVI (Model-View-Intent) — un caz particular al UDF cu trei elemente obligatorii: Intent (intenție), Model (stare), View (afișare). Diferența principală este că în MVI fiecare stare a ecranului este descrisă de o singură structură imutabilă (Sealed class), iar View este o funcție pură de la Model la UI. UDF este un termen mai larg care descrie orice flux unidirecțional, inclusiv Redux și Elm. În documentația Google termenul UDF este folosit ca denumire generală, iar MVI — ca implementare specifică.

Când este UDF excesiv?

UDF este excesiv pentru ecrane cu un singur câmp de intrare fără validare, pagini statice și ecrane de tip placeholder. Dacă ecranul nu are logică de afaceri și starea sa nu depinde de acțiunile utilizatorului, UDF adaugă cod inutil fără beneficii. Pentru astfel de scenarii este suficientă legarea unidirecțională simplă sau @State în SwiftUI. UDF este justificat când numărul de stări posibile ale ecranului depășește 3–4 și/sau sunt prezente efecte secundare.

Cum se testează UDF?

Deoarece Reducer este o funcție pură, testarea sa se reduce la apelarea cu diferite combinații de State și Intent și verificarea State și Effect rezultate. În Android utilizați Turbine pentru testarea StateFlow: trimiteți Intent, verificați următoarea emisie State. În TCA există TestStore încorporat, care verifică automat că după Action s-au modificat doar câmpurile așteptate ale State și au fost executate doar Effect-urile așteptate.

Se poate combina UDF cu Two-Way Binding?

Da, combinarea este permisă și adesea optimă. Pentru câmpurile de intrare din interiorul formularului utilizați Two-Way Binding local (sau Binding în SwiftUI) pentru a nu crea Intent la fiecare apăsare de tastă. La submit-ul formularului trimiteți un singur Intent cu datele colectate, care este procesat de Reducer. Această abordare hibridă — UDF global cu Two-Way Binding local — este folosită în 70% din aplicațiile comerciale SwiftUI (date Swift Community Survey 2024).

Ce au în comun UDF, Redux și Elm?

Toate cele trei modele implementează fluxul unidirecțional de date cu o singură sursă de adevăr. Elm (2012) — un limbaj funcțional care a introdus primul ciclul pur Model → View → Update. Redux (2015) a adaptat Elm pentru JavaScript cu conceptul de Store, Reducer și Action. UDF este generalizarea acestor idei pentru dezvoltarea mobilă. Toate cele trei abordări garantează predictibilitatea modificărilor prin actualizări atomice (indivizibile) ale stării.

Concluzii

  • Unidirectional Data Flow (UDF) — model cu ciclu unidirecțional State → View → Intent → Reducer, care asigură predictibilitatea modificărilor stării.
  • În Android UDF se implementează prin ViewModel + StateFlow + sealed class Intent, în iOS — prin TCA (Reducer + Store) sau Observable nativ.
  • Google recomandă UDF ca arhitectură principală pentru Jetpack Compose, începând cu 2023.
  • Reducer — funcție pură fără efecte secundare; toate cererile de rețea și operațiile cu baza de date sunt mutate în stratul Effect.
  • UDF elimină problema buclelor infinite Two-Way Binding prin procesarea explicită a Intent și Single Source of Truth.
  • Riscurile principale — efecte secundare în Reducer, Intent prea detaliate și corutine neterminate.
  • Abordarea hibridă (Two-Way Binding local în formular + UDF global) este optimă pentru majoritatea aplicațiilor comerciale.

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și