Î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) — 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.
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.
Î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.
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.
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.
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.
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 date | Nedefinit | Strict unidirecțional |
| Modificarea stării | Direct prin setText() | Numai prin Intent → Reducer |
| Single Source of Truth | Nu | Da |
| Testabilitatea reductorului | Scăzută | Ridicată (funcție pură) |
| Recomandarea Google | Abordare învechită | Principală pentru Compose |
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
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ă.
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.
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.
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).
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
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.
Citiți și