Comprenda qué es el Flujo de Datos Unidireccional — un flujo de datos en un solo sentido, un patrón arquitectónico donde los datos se mueven en un ciclo cerrado State → View → Intent → Reducer → State sin bucles de retroalimentación. A diferencia del enlace bidireccional, UDF garantiza que los cambios de estado ocurran solo mediante acciones explícitas (Intent/Event), haciendo que el flujo de datos sea predecible y rastreable. Según Google I/O 2024, UDF es la arquitectura recomendada para aplicaciones Jetpack Compose y SwiftUI con lógica de negocio de complejidad media y alta.
Puntos Clave
Flujo de Datos Unidireccional (UDF) es un patrón arquitectónico donde los datos se mueven en una dirección en un ciclo cerrado, eliminando bucles de retroalimentación entre View y Model. A diferencia del Two-Way Binding, donde un cambio en la UI actualiza inmediatamente el modelo, UDF requiere una acción explícita (Intent, Event, Action) para cada cambio de estado. Esto hace que el flujo de datos sea completamente predecible: en cualquier momento, se puede determinar qué acción llevó al estado actual.
El concepto de UDF proviene de frameworks web — Redux (JavaScript, 2015) y Elm (2012) — y fue adaptado para el desarrollo móvil. Según Google I/O 2024, UDF se ha convertido en la arquitectura recomendada para Jetpack Compose, reemplazando al clásico MVVM con LiveData. En iOS, un enfoque similar está implementado en The Composable Architecture (TCA) de Point-Free, utilizado por más del 15% de los desarrolladores iOS según la Encuesta de la Comunidad Swift (2024).
La principal ventaja de UDF es la Fuente Única de Verdad (Single Source of Truth, SSOT): todo el estado de la aplicación se almacena en un solo lugar y se modifica mediante operaciones estrictamente definidas. Esto simplifica la depuración, las pruebas y la reproducción de errores, ya que cada cambio de estado se registra y puede reproducirse reenviando los mismos Intents.
El ciclo básico de UDF consta de cuatro pasos: State (estado actual) se renderiza en View; el usuario realiza una acción que se convierte en un Intent; el Intent se procesa en un Reducer (función pura), que crea un nuevo State; el nuevo estado se pasa a la View para volver a renderizarse. Este ciclo se repite en cada evento de usuario o del sistema.
Cada elemento del ciclo tiene una responsabilidad estricta: State — un objeto inmutable que describe el estado de la pantalla en un momento específico; View — una función que renderiza el State; Intent — un valor que describe la intención del usuario (por ejemplo, LoginIntent.Submit); Reducer — una función pura sin efectos secundarios, que toma el State actual y el Intent y devuelve un nuevo State. Los efectos secundarios (solicitudes de red, operaciones de base de datos) se trasladan a una capa separada de Middleware o Effect.
Según el artículo de Google Android Architecture (2024), la pureza del Reducer es un requisito clave: si un Reducer contiene una llamada de red o una escritura en la base de datos, probar y depurar el flujo de datos se vuelve imposible. Todos los efectos secundarios deben ejecutarse en una corrutina de ViewModel o en una Swift Task antes de llamar al Reducer, y el resultado debe enviarse como un nuevo Intent.
En Android, la implementación de UDF se basa en tres componentes de Jetpack: ViewModel gestiona el ciclo de vida, StateFlow proporciona un flujo de estado reactivo, Intent (sealed class) describe todas las acciones posibles del usuario. La View se suscribe a StateFlow mediante collectAsState() en Compose u observe() en el sistema de Vistas.
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) }
)
}El ejemplo muestra el ciclo completo de UDF en Android: LoginIntent describe todas las acciones posibles (cambio de email, cambio de contraseña, envío del formulario), LoginState es el estado inmutable, LoginViewModel procesa los Intents y actualiza StateFlow, y la pantalla Compose se suscribe al estado mediante collectAsState(). Cada cambio de estado es el resultado del procesamiento de un Intent específico, lo que hace que el flujo de datos sea completamente transparente.
En iOS, UDF se implementa mediante The Composable Architecture (TCA) de Point-Free o el patrón Observable nativo con iOS 17+. TCA proporciona un ciclo listo de State + Action + Reducer + Store, donde Store es la fuente única de verdad, y la View se suscribe a los cambios mediante @Observable u 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("Iniciar sesión") { viewStore.send(.submit) }
}
}
}
}loginReducer es una función pura: no realiza solicitudes de red directamente, sino que devuelve un Effect que será ejecutado por el entorno de TCA. Esto permite probar el reductor de forma aislada, sustituyendo efectos en las pruebas. La View se suscribe a los cambios del Store mediante WithViewStore y envía Actions mediante send(). TCA maneja automáticamente la cancelación de efectos cuando se destruye el Store, evitando fugas de memoria.
MVVM y UDF a menudo se confunden, pero hay una diferencia fundamental entre ellos. MVVM es un patrón estructural que separa el código en tres capas (Model, View, ViewModel) pero no define la dirección del flujo de datos. UDF es un patrón de comportamiento que describe cómo se mueven los datos dentro de esa estructura. En MVVM con LiveData, tanto el enlace bidireccional como el flujo unidireccional son posibles — UDF añade reglas estrictas de procesamiento de Intent a MVVM.
Según la documentación de Android Developers (2024), la arquitectura recomendada para Compose es UDF dentro de MVVM: ViewModel almacena el State y procesa los Intents, View se suscribe al State y envía Intents. Google recomienda el MVVM clásico con Two-Way Binding mediante DataBinding solo para pantallas simples sin lógica de negocio. Para Jetpack Compose, el escenario principal es UDF con manejo explícito de eventos.
Tabla comparativa:
| Característica | MVVM (clásico) | MVVM + UDF |
|---|---|---|
| Flujo de datos | No definido | Estrictamente unidireccional |
| Cambio de estado | Directamente mediante setText() | Solo a través de Intent → Reducer |
| Fuente Única de Verdad | No | Sí |
| Capacidad de prueba del Reducer | Baja | Alta (función pura) |
| Recomendación de Google | Enfoque heredado | Principal para Compose |
El error más común son los efectos secundarios dentro del Reducer. Los desarrolladores acostumbrados a MVVM colocan solicitudes de red directamente en el manejador de Intent, lo que hace que el Reducer sea impuro y rompe la capacidad de prueba. Todos los efectos deben devolverse como un valor (Effect / SideEffect) y ejecutarse por la infraestructura del framework. En Android, se usan corrutinas en ViewModel para esto; en TCA — Effect.run.
El segundo error son Intents demasiado detallados. Cada pulsación de tecla, movimiento de deslizador y cambio de texto genera un Intent separado. Para campos de entrada esto es excesivo — en tales casos, es aceptable usar Binding con un flujo unidireccional dentro del formulario (estado local), y enviar un Intent global solo para acciones significativas (enviar, navegar).
El tercer error es la falta de manejo de cancelación de efectos. Si el usuario abandona la pantalla mientras una corrutina o Task aún se está ejecutando, el resultado puede aplicarse a una View ya destruida. En Android, use viewModelScope.cancel() o takeWhileActive(); en TCA, los efectos se cancelan automáticamente cuando se destruye el Store. Según Google Issue Tracker (2024), las fugas de corrutinas incompletas se encuentran entre las 5 principales causas de fallos en aplicaciones Compose.
Preguntas Frecuentes
MVI (Model-View-Intent) es un caso específico de UDF con tres elementos obligatorios: Intent (intención), Model (estado), View (visualización). La diferencia principal es que en MVI, cada estado de pantalla se describe mediante una única estructura inmutable (Sealed class), y View es una función pura de Model a UI. UDF es un término más amplio que describe cualquier flujo unidireccional, incluyendo Redux y Elm. En la documentación de Google, el término UDF se usa como nombre general, mientras que MVI es una implementación específica.
UDF es excesivo para pantallas con un solo campo de entrada sin validación, páginas estáticas y pantallas placeholder. Si una pantalla no tiene lógica de negocio y su estado no depende de las acciones del usuario, UDF añade código innecesario sin beneficio. Para tales escenarios, es suficiente el enlace unidireccional o un simple @State en SwiftUI. UDF se justifica cuando el número de estados posibles de la pantalla supera 3–4 y/o hay efectos secundarios presentes.
Dado que el Reducer es una función pura, probarlo se reduce a llamarlo con diferentes combinaciones de State e Intent y verificar el State y Effect resultantes. En Android, use Turbine para probar StateFlow: envíe un Intent, verifique la siguiente emisión de State. En TCA, hay un TestStore incorporado que verifica automáticamente que después de una Action solo cambiaron los campos esperados de State y solo se ejecutaron los Effects esperados.
Sí, combinarlos es aceptable y a menudo óptimo. Para campos de entrada dentro de un formulario, use Two-Way Binding local (o Binding en SwiftUI) para evitar crear un Intent en cada pulsación de tecla. Al enviar el formulario, envíe un único Intent con los datos recopilados, que es procesado por el Reducer. Este enfoque híbrido — UDF global con Two-Way Binding local — se utiliza en el 70% de las aplicaciones comerciales de SwiftUI (datos de la Swift Community Survey 2024).
Los tres patrones implementan un flujo de datos unidireccional con una única fuente de verdad. Elm (2012) — un lenguaje funcional — introdujo por primera vez el ciclo puro Model → View → Update. Redux (2015) adaptó Elm para JavaScript con los conceptos de Store, Reducer y Action. UDF es una generalización de estas ideas para el desarrollo móvil. Los tres enfoques garantizan la previsibilidad de los cambios mediante actualizaciones atómicas del estado.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también