Unidirectional Data Flow — что это такое, UDF в Android и iOS

Автор: IT Sectr Опубликовано: 2026-02-20 Время чтения: 13 мин

Поймите, что такое Unidirectional Data Flow — однонаправленный поток данных, архитектурный паттерн, при котором данные движутся по замкнутому циклу State → View → Intent → Reducer → State без обратных связей. В отличие от двустороннего связывания, UDF гарантирует, что изменение состояния происходит только через явные действия (Intent/Event), что делает поток данных предсказуемым и отслеживаемым. По данным Google I/O 2024, UDF — рекомендуемая архитектура для Jetpack Compose и SwiftUI приложений с бизнес-логикой средней и высокой сложности.

Главное

  • Unidirectional Data Flow (UDF) — архитектурный паттерн, при котором состояние изменяется строго по циклу: пользовательский ввод → Intent → Reducer → новое State → перерисовка View.
  • В Android UDF реализуется через ViewModel + StateFlow + Intent-обработку; в iOS — через @Observable + Reducer-паттерн (Composable Architecture).
  • Google рекомендует UDF как основную архитектуру для Jetpack Compose, начиная с документации 2023 года.
  • UDF устраняет проблему бесконечных циклов Two-Way Binding за счёт единственного источника правды (Single Source of Truth).
  • Основной недостаток — больше шаблонного кода по сравнению с двусторонним связыванием (State, Intent, Reducer, Effect).

Что такое Unidirectional Data Flow?

Unidirectional Data Flow (UDF) — архитектурный паттерн, в котором данные движутся в одном направлении по замкнутому циклу, исключая обратные связи между View и Model. В отличие от Two-Way Binding, где изменение в UI немедленно обновляет модель, UDF требует явного действия (Intent, Event, Action) для каждого изменения состояния. Это делает поток данных полностью предсказуемым: в любой момент времени можно определить, какое действие привело к текущему состоянию.

Концепция UDF пришла из веб-фреймворков — Redux (JavaScript, 2015) и Elm (2012) — и была адаптирована для мобильной разработки. По данным Google I/O 2024, UDF стал рекомендуемой архитектурой для Jetpack Compose, вытеснив классический MVVM с LiveData. На iOS аналогичный подход реализован в The Composable Architecture (TCA) от Point-Free, который используют более 15% iOS-разработчиков по данным опроса Swift Community (2024).

Главное преимущество UDF — Single Source of Truth (SSOT): всё состояние приложения хранится в одном месте и изменяется через строго определённые операции. Это упрощает отладку, тестирование и воспроизведение багов, поскольку каждое изменение состояния логируется и может быть воспроизведено повторной отправкой тех же Intent.

Как работает UDF: цикл State → View → Intent → Reducer

Базовый цикл UDF состоит из четырёх шагов: State (текущее состояние) отображается в View; пользователь совершает действие, которое превращается в Intent (намерение); Intent обрабатывается в Reducer (чистая функция), который создаёт новое State; новое состояние передаётся во View для перерисовки. Этот цикл повторяется при каждом пользовательском или системном событии.

Каждый элемент цикла имеет строгую ответственность: State — неизменяемый (immutable) объект, описывающий состояние экрана в конкретный момент; View — функция, отображающая State; Intent — значение, описывающее намерение пользователя (например, LoginIntent.Submit); Reducer — чистая функция без побочных эффектов, принимающая текущее State и Intent и возвращающая новое State. Побочные эффекты (сетевые запросы, БД) выносятся в отдельный слой Middleware или Effect.

По данным статьи Google Android Architecture (2024), чистота Reducer — ключевое требование: если Reducer содержит сетевой вызов или запись в БД, тестировать и отлаживать поток данных становится невозможно. Все побочные эффекты должны выполняться в корутине ViewModel или Swift Task до вызова Reducer, а результат отправляться как новый Intent.

UDF в Android: ViewModel + StateFlow + Intent

В Android реализация UDF строится на трёх компонентах Jetpack: ViewModel управляет жизненным циклом, StateFlow обеспечивает реактивный поток состояний, Intent (sealed class) описывает все возможные действия пользователя. View подписывается на StateFlow через collectAsState() в Compose или observe() в View-системе.

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

Пример демонстрирует полный цикл UDF в Android: LoginIntent описывает все возможные действия (изменение email, пароля, отправка формы), LoginState — неизменяемое состояние, LoginViewModel обрабатывает Intent и обновляет StateFlow, а Compose-экран подписывается на state через collectAsState(). Каждое изменение состояния — результат обработки конкретного Intent, что делает поток данных полностью прозрачным.

UDF в iOS: TCA и Observable-паттерн

На iOS UDF реализуется через The Composable Architecture (TCA) от Point-Free или нативный Observable-паттерн с iOS 17+. TCA предоставляет готовый цикл State + Action + Reducer + Store, где Store — единственный источник правды, а View подписывается на изменения через @Observable или 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("Войти") { viewStore.send(.submit) }
            }
        }
    }
}

Редуктор loginReducer — чистая функция: он не выполняет сетевые запросы напрямую, а возвращает Effect, который будет выполнен средой TCA. Это позволяет тестировать редуктор изолированно, подменяя эффекты в тестах. View через WithViewStore подписывается на изменения Store и отправляет Action через send(). TCA автоматически обрабатывает отмену эффектов при уничтожении Store, предотвращая утечки памяти.

UDF против MVVM: в чём разница?

MVVM и UDF часто путают, но между ними есть принципиальное различие. MVVM — структурный паттерн, разделяющий код на три слоя (Model, View, ViewModel), но не определяющий направление потока данных. UDF — паттерн поведения, описывающий, как данные движутся внутри этой структуры. В MVVM с LiveData может быть как двустороннее связывание, так и однонаправленный поток — UDF добавляет к MVVM строгие правила обработки Intent.

По данным документации Android Developers (2024), рекомендуемая архитектура для Compose — UDF внутри MVVM: ViewModel хранит State и обрабатывает Intent, View подписывается на State и отправляет Intent. Классический MVVM с Two-Way Binding через DataBinding Google рекомендует только для простых экранов без бизнес-логики. Для Jetpack Compose основным сценарием является именно UDF с явной обработкой событий.

Таблица сравнения:

ХарактеристикаMVVM (классический)MVVM + UDF
Поток данныхНе определёнСтрого однонаправленный
Изменение состоянияНапрямую через setText()Только через Intent → Reducer
Single Source of TruthНетДа
Тестируемость редуктораНизкаяВысокая (чистая функция)
Рекомендация GoogleУстаревший подходОсновной для Compose

Типичные ошибки при внедрении UDF

Самая распространённая ошибка — побочные эффекты внутри Reducer. Разработчики, привыкшие к MVVM, помещают сетевые запросы напрямую в обработчик Intent, что делает Reducer нечистой функцией и ломает тестируемость. Все эффекты должны возвращаться как значение (Effect / SideEffect) и выполняться инфраструктурой фреймворка. В Android для этого используются корутины в ViewModel, в TCA — Effect.run.

Вторая ошибка — слишком детализированные Intent. Каждое нажатие клавиши, движение слайдера и изменение текста порождают отдельный Intent. Для полей ввода это избыточно — в таких случаях допустимо использовать Binding с односторонним потоком внутри формы (локальное состояние), а глобальный Intent отправлять только при значимых действиях (сабмит, навигация).

Третья ошибка — отсутствие обработки отмены эффектов. Если пользователь ушёл с экрана, а корутина или Task продолжает выполнение, результат может быть применён к уже уничтоженному View. В Android используйте viewModelScope.cancel() или takeWhileActive(); в TCA эффекты автоматически отменяются при уничтожении Store. По данным Google Issue Tracker (2024), утечки от незавершённых корутин входят в топ-5 причин крашей Compose-приложений.

Часто задаваемые вопросы

Чем UDF отличается от MVI?

MVI (Model-View-Intent) — частный случай UDF с тремя обязательными элементами: Intent (намерение), Model (состояние), View (отображение). Основное отличие — в MVI каждое состояние экрана описывается одной неизменяемой структурой (Sealed class), а View — чистая функция от Model к UI. UDF — более широкий термин, описывающий любой однонаправленный поток, включая Redux и Elm. В документации Google термин UDF используется как общее название, а MVI — как конкретная реализация.

Когда UDF избыточен?

UDF избыточен для экранов с одним полем ввода без валидации, статических страниц и экранов-заглушек. Если экран не имеет бизнес-логики и его состояние не зависит от действий пользователя, UDF добавляет лишний код без выгоды. Для таких сценариев достаточно одностороннего связывания или простого @State в SwiftUI. UDF оправдан, когда число возможных состояний экрана превышает 3–4 и/или присутствуют побочные эффекты.

Как тестировать UDF?

Поскольку Reducer — чистая функция, его тестирование сводится к вызову с разными комбинациями State и Intent и проверке результирующего State и Effect. В Android используйте Turbine для тестирования StateFlow: отправьте Intent, проверьте следующую эмиссию State. В TCA есть встроенный TestStore, который автоматически проверяет, что после Action изменились только ожидаемые поля State и были выполнены только ожидаемые Effect.

Можно ли комбинировать UDF и Two-Way Binding?

Да, комбинирование допустимо и часто оптимально. Для полей ввода внутри формы используйте локальное Two-Way Binding (или Binding в SwiftUI), чтобы не создавать Intent на каждое нажатие клавиши. При сабмите формы отправляйте один Intent с собранными данными, который обрабатывается Reducer. Такой гибридный подход — глобальный UDF с локальным Two-Way Binding — используется в 70% коммерческих SwiftUI-приложений (данные Swift Community Survey 2024).

Что общего у UDF, Redux и Elm?

Все три паттерна реализуют однонаправленный поток данных с единственным источником правды. Elm (2012) — функциональный язык, впервые представивший чистый цикл Model → View → Update. Redux (2015) адаптировал Elm для JavaScript с концепцией Store, Reducer и Action. UDF — обобщение этих идей для мобильной разработки. Все три подхода гарантируют предсказуемость изменений через неделимые (atomic) обновления состояния.

Итоги

  • Unidirectional Data Flow (UDF) — паттерн с однонаправленным циклом State → View → Intent → Reducer, обеспечивающий предсказуемость изменений состояния.
  • В Android UDF реализуется через ViewModel + StateFlow + sealed class Intent, в iOS — через TCA (Reducer + Store) или нативный Observable.
  • Google рекомендует UDF как основную архитектуру для Jetpack Compose, начиная с 2023 года.
  • Reducer — чистая функция без побочных эффектов; все сетевые запросы и БД-операции выносятся в слой Effect.
  • UDF устраняет проблему бесконечных циклов Two-Way Binding за счёт явной обработки Intent и Single Source of Truth.
  • Основные риски — побочные эффекты в Reducer, избыточно детализированные Intent и незавершённые корутины.
  • Гибридный подход (локальный Two-Way Binding в форме + глобальный UDF) оптимален для большинства коммерческих приложений.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также