Comprendi cos'è il Flusso di Dati Unidirezionale — un flusso di dati a senso unico, un pattern architetturale in cui i dati si muovono in un ciclo chiuso State → View → Intent → Reducer → State senza cicli di retroazione. A differenza del binding bidirezionale, UDF garantisce che i cambiamenti di stato avvengano solo attraverso azioni esplicite (Intent/Event), rendendo il flusso di dati prevedibile e tracciabile. Secondo Google I/O 2024, UDF è l'architettura raccomandata per applicazioni Jetpack Compose e SwiftUI con logica di business di media e alta complessità.
Punti Chiave
Flusso di Dati Unidirezionale (UDF) è un pattern architetturale in cui i dati si muovono in una direzione in un ciclo chiuso, eliminando i cicli di retroazione tra View e Model. A differenza del Two-Way Binding, dove un cambiamento nell'UI aggiorna immediatamente il modello, UDF richiede un'azione esplicita (Intent, Event, Action) per ogni cambiamento di stato. Questo rende il flusso di dati completamente prevedibile: in qualsiasi momento, si può determinare quale azione ha portato allo stato corrente.
Il concetto di UDF proviene dai framework web — Redux (JavaScript, 2015) e Elm (2012) — ed è stato adattato per lo sviluppo mobile. Secondo Google I/O 2024, UDF è diventato l'architettura raccomandata per Jetpack Compose, sostituendo il classico MVVM con LiveData. Su iOS, un approccio simile è implementato in The Composable Architecture (TCA) di Point-Free, utilizzato da oltre il 15% degli sviluppatori iOS secondo il Swift Community Survey (2024).
Il principale vantaggio di UDF è la Singola Fonte di Verità (Single Source of Truth, SSOT): tutto lo stato dell'applicazione è memorizzato in un unico luogo e modificato tramite operazioni strettamente definite. Questo semplifica il debug, il testing e la riproduzione dei bug, poiché ogni cambiamento di stato viene registrato e può essere riprodotto inviando nuovamente gli stessi Intent.
Il ciclo base di UDF si compone di quattro passaggi: State (stato corrente) viene renderizzato nella View; l'utente esegue un'azione che diventa un Intent; l'Intent viene elaborato in un Reducer (funzione pura), che crea un nuovo State; il nuovo stato viene passato alla View per il re-rendering. Questo ciclo si ripete ad ogni evento utente o di sistema.
Ogni elemento del ciclo ha una responsabilità rigorosa: State — un oggetto immutabile che descrive lo stato dello schermo in un momento specifico; View — una funzione che renderizza lo State; Intent — un valore che descrive l'intenzione dell'utente (ad esempio, LoginIntent.Submit); Reducer — una funzione pura senza effetti collaterali, che riceve lo State corrente e l'Intent e restituisce un nuovo State. Gli effetti collaterali (richieste di rete, operazioni sul database) vengono spostati in un livello separato di Middleware o Effect.
Secondo l'articolo Google Android Architecture (2024), la purezza del Reducer è un requisito fondamentale: se un Reducer contiene una chiamata di rete o una scrittura sul database, diventa impossibile testare e debuggare il flusso di dati. Tutti gli effetti collaterali devono essere eseguiti in una coroutine ViewModel o in una Swift Task prima di chiamare il Reducer, e il risultato deve essere inviato come un nuovo Intent.
Su Android, l'implementazione di UDF si basa su tre componenti Jetpack: ViewModel gestisce il ciclo di vita, StateFlow fornisce un flusso di stato reattivo, Intent (sealed class) descrive tutte le possibili azioni dell'utente. La View si iscrive a StateFlow tramite collectAsState() in Compose o observe() nel sistema di 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) }
)
}L'esempio mostra il ciclo UDF completo in Android: LoginIntent descrive tutte le azioni possibili (cambio email, cambio password, invio del form), LoginState è lo stato immutabile, LoginViewModel elabora gli Intent e aggiorna StateFlow, e lo schermo Compose si iscrive allo stato tramite collectAsState(). Ogni cambiamento di stato è il risultato dell'elaborazione di un Intent specifico, rendendo il flusso di dati completamente trasparente.
Su iOS, UDF è implementato tramite The Composable Architecture (TCA) di Point-Free o il pattern Observable nativo con iOS 17+. TCA fornisce un ciclo pronto di State + Action + Reducer + Store, dove Store è l'unica fonte di verità, e la View si iscrive ai cambiamenti tramite @Observable o 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("Accedi") { viewStore.send(.submit) }
}
}
}
}loginReducer è una funzione pura: non esegue richieste di rete direttamente ma restituisce un Effect che sarà eseguito dall'ambiente TCA. Questo permette di testare il riduttore in isolamento sostituendo gli effetti nei test. La View si iscrive ai cambiamenti dello Store tramite WithViewStore e invia Actions tramite send(). TCA gestisce automaticamente l'annullamento degli effetti quando lo Store viene distrutto, prevenendo perdite di memoria.
MVVM e UDF sono spesso confusi, ma c'è una differenza fondamentale tra loro. MVVM è un pattern strutturale che separa il codice in tre livelli (Model, View, ViewModel) ma non definisce la direzione del flusso di dati. UDF è un pattern comportamentale che descrive come i dati si muovono all'interno di quella struttura. In MVVM con LiveData, sia il binding bidirezionale che il flusso unidirezionale sono possibili — UDF aggiunge regole rigorose di elaborazione degli Intent a MVVM.
Secondo la documentazione di Android Developers (2024), l'architettura raccomandata per Compose è UDF all'interno di MVVM: ViewModel memorizza lo State e elabora gli Intent, la View si iscrive allo State e invia Intent. Google raccomanda il classico MVVM con Two-Way Binding tramite DataBinding solo per schermi semplici senza logica di business. Per Jetpack Compose, lo scenario principale è UDF con gestione esplicita degli eventi.
Tabella comparativa:
| Caratteristica | MVVM (classico) | MVVM + UDF |
|---|---|---|
| Flusso di dati | Non definito | Strettamente unidirezionale |
| Cambiamento di stato | Direttamente tramite setText() | Solo tramite Intent → Reducer |
| Singola Fonte di Verità | No | Sì |
| Testabilità del Reducer | Bassa | Alta (funzione pura) |
| Raccomandazione Google | Approccio legacy | Principale per Compose |
L'errore più comune sono gli effetti collaterali all'interno del Reducer. Gli sviluppatori abituati a MVVM inseriscono le richieste di rete direttamente nel gestore dell'Intent, rendendo il Reducer impuro e compromettendo la testabilità. Tutti gli effetti devono essere restituiti come un valore (Effect / SideEffect) ed eseguiti dall'infrastruttura del framework. Su Android, si usano le coroutine in ViewModel per questo; in TCA — Effect.run.
Il secondo errore sono Intent eccessivamente dettagliati. Ogni pressione di tasto, movimento di cursore e cambiamento di testo genera un Intent separato. Per i campi di input questo è eccessivo — in tali casi, è accettabile usare Binding con un flusso unidirezionale all'interno del form (stato locale) e inviare un Intent globale solo per azioni significative (invio, navigazione).
Il terzo errore è la mancanza di gestione dell'annullamento degli effetti. Se l'utente lascia lo schermo mentre una coroutine o Task è ancora in esecuzione, il risultato potrebbe essere applicato a una View già distrutta. Su Android, usa viewModelScope.cancel() o takeWhileActive(); in TCA, gli effetti vengono automaticamente annullati quando lo Store viene distrutto. Secondo Google Issue Tracker (2024), le perdite da coroutine incomplete sono tra le prime 5 cause di crash nelle applicazioni Compose.
Domande Frequenti
MVI (Model-View-Intent) è un caso specifico di UDF con tre elementi obbligatori: Intent (intenzione), Model (stato), View (visualizzazione). La differenza principale è che in MVI, ogni stato dello schermo è descritto da un'unica struttura immutabile (Sealed class) e View è una funzione pura da Model a UI. UDF è un termine più ampio che descrive qualsiasi flusso unidirezionale, inclusi Redux ed Elm. Nella documentazione Google, il termine UDF viene usato come nome generale, mentre MVI è un'implementazione specifica.
UDF è eccessivo per schermi con un singolo campo di input senza validazione, pagine statiche e schermi placeholder. Se uno schermo non ha logica di business e il suo stato non dipende dalle azioni dell'utente, UDF aggiunge codice inutile senza beneficio. Per tali scenari, sono sufficienti il binding unidirezionale o un semplice @State in SwiftUI. UDF è giustificato quando il numero di possibili stati dello schermo supera 3–4 e/o sono presenti effetti collaterali.
Poiché il Reducer è una funzione pura, testarlo si riduce a chiamarlo con diverse combinazioni di State e Intent e verificare lo State e l'Effect risultanti. Su Android, usa Turbine per testare StateFlow: invia un Intent, verifica la successiva emissione di State. In TCA, c'è un TestStore integrato che verifica automaticamente che dopo un Action siano cambiati solo i campi State previsti e siano stati eseguiti solo gli Effects previsti.
Sì, combinarli è accettabile e spesso ottimale. Per i campi di input all'interno di un form, usa il Two-Way Binding locale (o Binding in SwiftUI) per evitare di creare un Intent ad ogni pressione di tasto. All'invio del form, invia un singolo Intent con i dati raccolti, che viene elaborato dal Reducer. Questo approccio ibrido — UDF globale con Two-Way Binding locale — è utilizzato nel 70% delle applicazioni SwiftUI commerciali (dati Swift Community Survey 2024).
Tutti e tre i pattern implementano un flusso di dati unidirezionale con un'unica fonte di verità. Elm (2012) — un linguaggio funzionale — ha introdotto per primo il ciclo puro Model → View → Update. Redux (2015) ha adattato Elm per JavaScript con i concetti di Store, Reducer e Action. UDF è una generalizzazione di queste idee per lo sviluppo mobile. Tutti e tre gli approcci garantiscono la prevedibilità dei cambiamenti tramite aggiornamenti atomici dello stato.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche