Flujo de Datos Unidireccional — Qué Es, UDF en Android y iOS

Autor: IT Sectr Publicado: 2026-02-20 Tiempo de lectura: 13 min

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) — un patrón arquitectónico donde el estado cambia estrictamente siguiendo el ciclo: entrada del usuario → Intent → Reducer → nuevo State → renderizado de la View.
  • En Android, UDF se implementa mediante ViewModel + StateFlow + manejo de Intent; en iOS — mediante @Observable + patrón Reducer (Composable Architecture).
  • Google recomienda UDF como la arquitectura principal para Jetpack Compose, a partir de la documentación de 2023.
  • UDF elimina el problema de bucles infinitos del Two-Way Binding mediante una Fuente Única de Verdad (Single Source of Truth).
  • La principal desventaja es más código repetitivo en comparación con el enlace bidireccional (State, Intent, Reducer, Effect).

¿Qué es el Flujo de Datos Unidireccional?

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.

Cómo funciona UDF: ciclo State → View → Intent → Reducer

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.

UDF en Android: ViewModel + StateFlow + 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.

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) }
    )
}

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.

UDF en iOS: TCA y patrón Observable

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.

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("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.

UDF vs MVVM: ¿Cuál es la diferencia?

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ísticaMVVM (clásico)MVVM + UDF
Flujo de datosNo definidoEstrictamente unidireccional
Cambio de estadoDirectamente mediante setText()Solo a través de Intent → Reducer
Fuente Única de VerdadNo
Capacidad de prueba del ReducerBajaAlta (función pura)
Recomendación de GoogleEnfoque heredadoPrincipal para Compose

Errores comunes al implementar UDF

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

¿En qué se diferencia UDF de MVI?

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.

¿Cuándo es excesivo UDF?

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.

¿Cómo probar UDF?

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.

¿Se pueden combinar UDF y Two-Way Binding?

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).

¿Qué tienen en común UDF, Redux y Elm?

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

  • Flujo de Datos Unidireccional (UDF) es un patrón con un ciclo unidireccional State → View → Intent → Reducer, que garantiza cambios de estado predecibles.
  • En Android, UDF se implementa mediante ViewModel + StateFlow + sealed class Intent; en iOS — mediante TCA (Reducer + Store) u Observable nativo.
  • Google recomienda UDF como la arquitectura principal para Jetpack Compose, a partir de 2023.
  • El Reducer es una función pura sin efectos secundarios; todas las solicitudes de red y operaciones de base de datos se trasladan a la capa de Effect.
  • UDF elimina el problema de bucles infinitos del Two-Way Binding mediante el manejo explícito de Intent y una Fuente Única de Verdad.
  • Los principales riesgos — efectos secundarios en el Reducer, Intents demasiado detallados y corrutinas incompletas.
  • Un enfoque híbrido (Two-Way Binding local en formularios + UDF global) es óptimo para la mayoría de las aplicaciones comerciales.

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.

Discutir el proyecto

Lea también