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 описује све могуће акције (промена имејла, лозинке, слање форме), 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 постављају мрежне захтеве директно у handler 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 је генерализација ових идеја за мобилни развој. Сва три приступа гарантују предвидљивост промена кроз недељива (атомска) ажурирања стања.

Завршне напомене

  • 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. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође