Fluxo de Dados Unidirecional — O Que É, UDF no Android e iOS

Autor: IT Sectr Publicado: 2026-02-20 Tempo de leitura: 13 min

Entenda o que é o Fluxo de Dados Unidirecional — um fluxo de dados em sentido único, um padrão arquitetural onde os dados se movem em um ciclo fechado State → View → Intent → Reducer → State sem loops de realimentação. Ao contrário da ligação bidirecional, o UDF garante que as mudanças de estado ocorram apenas por meio de ações explícitas (Intent/Event), tornando o fluxo de dados previsível e rastreável. De acordo com o Google I/O 2024, o UDF é a arquitetura recomendada para aplicativos Jetpack Compose e SwiftUI com lógica de negócios de média e alta complexidade.

Principais Pontos

  • Fluxo de Dados Unidirecional (UDF) — um padrão arquitetural onde o estado muda estritamente seguindo o ciclo: entrada do usuário → Intent → Reducer → novo State → renderização da View.
  • No Android, o UDF é implementado via ViewModel + StateFlow + manipulação de Intent; no iOS — via @Observable + padrão Reducer (Composable Architecture).
  • O Google recomenda o UDF como arquitetura principal para Jetpack Compose, a partir da documentação de 2023.
  • O UDF elimina o problema de loops infinitos do Two-Way Binding por meio de uma Fonte Única de Verdade (Single Source of Truth).
  • A principal desvantagem é mais código boilerplate em comparação com a ligação bidirecional (State, Intent, Reducer, Effect).

O Que É Fluxo de Dados Unidirecional?

Fluxo de Dados Unidirecional (UDF) é um padrão arquitetural onde os dados se movem em uma direção em um ciclo fechado, eliminando loops de realimentação entre View e Model. Ao contrário do Two-Way Binding, onde uma mudança na UI atualiza imediatamente o modelo, o UDF requer uma ação explícita (Intent, Event, Action) para cada mudança de estado. Isso torna o fluxo de dados completamente previsível: a qualquer momento, pode-se determinar qual ação levou ao estado atual.

O conceito de UDF veio de frameworks web — Redux (JavaScript, 2015) e Elm (2012) — e foi adaptado para o desenvolvimento móvel. De acordo com o Google I/O 2024, o UDF se tornou a arquitetura recomendada para Jetpack Compose, substituindo o MVVM clássico com LiveData. No iOS, uma abordagem similar é implementada no The Composable Architecture (TCA) da Point-Free, usado por mais de 15% dos desenvolvedores iOS de acordo com a Swift Community Survey (2024).

A principal vantagem do UDF é a Fonte Única de Verdade (Single Source of Truth, SSOT): todo o estado do aplicativo é armazenado em um só lugar e modificado através de operações estritamente definidas. Isso simplifica a depuração, os testes e a reprodução de bugs, já que cada mudança de estado é registrada e pode ser reproduzida reenviando os mesmos Intents.

Como o UDF Funciona: ciclo State → View → Intent → Reducer

O ciclo básico do UDF consiste em quatro etapas: State (estado atual) é renderizado na View; o usuário realiza uma ação que se torna um Intent; o Intent é processado em um Reducer (função pura), que cria um novo State; o novo estado é passado para a View para re-renderização. Este ciclo se repete a cada evento do usuário ou do sistema.

Cada elemento do ciclo tem uma responsabilidade estrita: State — um objeto imutável descrevendo o estado da tela em um momento específico; View — uma função que renderiza o State; Intent — um valor descrevendo a intenção do usuário (por exemplo, LoginIntent.Submit); Reducer — uma função pura sem efeitos colaterais, que recebe o State atual e o Intent e retorna um novo State. Os efeitos colaterais (requisições de rede, operações de banco de dados) são movidos para uma camada separada de Middleware ou Effect.

De acordo com o artigo Google Android Architecture (2024), a pureza do Reducer é um requisito fundamental: se um Reducer contém uma chamada de rede ou gravação no banco de dados, testar e depurar o fluxo de dados se torna impossível. Todos os efeitos colaterais devem ser executados em uma corrotina ViewModel ou Swift Task antes de chamar o Reducer, e o resultado deve ser enviado como um novo Intent.

UDF no Android: ViewModel + StateFlow + Intent

No Android, a implementação do UDF é construída sobre três componentes do Jetpack: ViewModel gerencia o ciclo de vida, StateFlow fornece um fluxo de estado reativo, Intent (sealed class) descreve todas as ações possíveis do usuário. A View se inscreve no StateFlow via collectAsState() no Compose ou observe() no sistema de Views.

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

O exemplo demonstra o ciclo UDF completo no Android: LoginIntent descreve todas as ações possíveis (alteração de email, alteração de senha, envio do formulário), LoginState é o estado imutável, LoginViewModel processa os Intents e atualiza o StateFlow, e a tela Compose se inscreve no estado via collectAsState(). Cada mudança de estado é o resultado do processamento de um Intent específico, tornando o fluxo de dados completamente transparente.

UDF no iOS: TCA e padrão Observable

No iOS, o UDF é implementado através The Composable Architecture (TCA) da Point-Free ou do padrão Observable nativo com iOS 17+. O TCA fornece um ciclo pronto de State + Action + Reducer + Store, onde Store é a fonte única de verdade, e a View se inscreve nas mudanças via @Observable ou 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("Entrar") { viewStore.send(.submit) }
            }
        }
    }
}

O loginReducer é uma função pura: ele não realiza requisições de rede diretamente, mas retorna um Effect que será executado pelo ambiente TCA. Isso permite testar o redutor isoladamente, substituindo efeitos nos testes. A View se inscreve nas mudanças da Store através de WithViewStore e envia Actions via send(). O TCA lida automaticamente com o cancelamento de efeitos quando a Store é destruída, prevenindo vazamentos de memória.

UDF vs MVVM: Qual é a diferença?

MVVM e UDF são frequentemente confundidos, mas há uma diferença fundamental entre eles. MVVM é um padrão estrutural que separa o código em três camadas (Model, View, ViewModel) mas não define a direção do fluxo de dados. UDF é um padrão comportamental que descreve como os dados se movem dentro dessa estrutura. No MVVM com LiveData, tanto a ligação bidirecional quanto o fluxo unidirecional são possíveis — o UDF adiciona regras estritas de processamento de Intent ao MVVM.

De acordo com a documentação Android Developers (2024), a arquitetura recomendada para Compose é UDF dentro do MVVM: ViewModel armazena o State e processa os Intents, View se inscreve no State e envia Intents. O Google recomenda o MVVM clássico com Two-Way Binding via DataBinding apenas para telas simples sem lógica de negócios. Para Jetpack Compose, o cenário principal é UDF com manipulação explícita de eventos.

Tabela comparativa:

CaracterísticaMVVM (clássico)MVVM + UDF
Fluxo de dadosNão definidoEstritamente unidirecional
Mudança de estadoDiretamente via setText()Apenas através de Intent → Reducer
Fonte Única de VerdadeNãoSim
Testabilidade do ReducerBaixaAlta (função pura)
Recomendação do GoogleAbordagem legadaPrincipal para Compose

Erros comuns ao implementar UDF

O erro mais comum são efeitos colaterais dentro do Reducer. Desenvolvedores acostumados ao MVVM colocam requisições de rede diretamente no manipulador de Intent, tornando o Reducer impuro e quebrando a testabilidade. Todos os efeitos devem ser retornados como um valor (Effect / SideEffect) e executados pela infraestrutura do framework. No Android, corrotinas no ViewModel são usadas para isso; no TCA — Effect.run.

O segundo erro são Intents excessivamente detalhados. Cada pressionamento de tecla, movimento de controle deslizante e alteração de texto gera um Intent separado. Para campos de entrada isso é excessivo — nesses casos, é aceitável usar Binding com um fluxo unidirecional dentro do formulário (estado local), e enviar um Intent global apenas para ações significativas (enviar, navegar).

O terceiro erro é falta de tratamento de cancelamento de efeitos. Se o usuário sai da tela enquanto uma corrotina ou Task ainda está em execução, o resultado pode ser aplicado a uma View já destruída. No Android, use viewModelScope.cancel() ou takeWhileActive(); no TCA, os efeitos são automaticamente cancelados quando a Store é destruída. De acordo com o Google Issue Tracker (2024), vazamentos de corrotinas incompletas estão entre as 5 principais causas de falhas em aplicativos Compose.

Perguntas Frequentes

Como o UDF difere do MVI?

MVI (Model-View-Intent) é um caso específico de UDF com três elementos obrigatórios: Intent (intenção), Model (estado), View (exibição). A principal diferença é que no MVI, cada estado de tela é descrito por uma única estrutura imutável (Sealed class), e View é uma função pura de Model para UI. UDF é um termo mais amplo que descreve qualquer fluxo unidirecional, incluindo Redux e Elm. Na documentação do Google, o termo UDF é usado como nome geral, enquanto MVI é uma implementação específica.

Quando o UDF é excessivo?

UDF é excessivo para telas com um único campo de entrada sem validação, páginas estáticas e telas placeholder. Se uma tela não tem lógica de negócios e seu estado não depende das ações do usuário, UDF adiciona código desnecessário sem benefício. Para tais cenários, ligação unidirecional ou simples @State no SwiftUI são suficientes. UDF se justifica quando o número de estados possíveis da tela excede 3–4 e/ou efeitos colaterais estão presentes.

Como testar UDF?

Como o Reducer é uma função pura, testá-lo se resume a chamá-lo com diferentes combinações de State e Intent e verificar o State e Effect resultantes. No Android, use Turbine para testar StateFlow: envie um Intent, verifique a próxima emissão de State. No TCA, há um TestStore integrado que verifica automaticamente que após uma Action apenas os campos esperados de State mudaram e apenas os Effects esperados foram executados.

Pode-se combinar UDF e Two-Way Binding?

Sim, combiná-los é aceitável e muitas vezes ótimo. Para campos de entrada dentro de um formulário, use Two-Way Binding local (ou Binding no SwiftUI) para evitar criar um Intent a cada pressionamento de tecla. Ao enviar o formulário, envie um único Intent com os dados coletados, que é processado pelo Reducer. Esta abordagem híbrida — UDF global com Two-Way Binding local — é usada em 70% dos aplicativos SwiftUI comerciais (dados da Swift Community Survey 2024).

O que UDF, Redux e Elm têm em comum?

Os três padrões implementam fluxo de dados unidirecional com uma única fonte de verdade. Elm (2012) — uma linguagem funcional — introduziu pela primeira vez o ciclo puro Model → View → Update. Redux (2015) adaptou Elm para JavaScript com os conceitos de Store, Reducer e Action. UDF é uma generalização dessas ideias para o desenvolvimento móvel. As três abordagens garantem previsibilidade de mudanças através de atualizações atômicas de estado.

Resumo

  • Fluxo de Dados Unidirecional (UDF) é um padrão com um ciclo unidirecional State → View → Intent → Reducer, garantindo mudanças de estado previsíveis.
  • No Android, UDF é implementado via ViewModel + StateFlow + sealed class Intent; no iOS — via TCA (Reducer + Store) ou Observable nativo.
  • O Google recomenda UDF como arquitetura principal para Jetpack Compose, a partir de 2023.
  • O Reducer é uma função pura sem efeitos colaterais; todas as requisições de rede e operações de banco de dados são movidas para a camada Effect.
  • UDF elimina o problema de loops infinitos do Two-Way Binding através do tratamento explícito de Intent e de uma Fonte Única de Verdade.
  • Principais riscos — efeitos colaterais no Reducer, Intents excessivamente detalhados e corrotinas incompletas.
  • Uma abordagem híbrida (Two-Way Binding local em formulários + UDF global) é ótima para a maioria dos aplicativos comerciais.

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também