Tek Yönlü Veri Akışı'nın ne olduğunu anlayın — tek yönlü bir veri akışı, verilerin geri bildirim döngüleri olmadan kapalı bir döngü State → View → Intent → Reducer → State içinde hareket ettiği bir mimari desen. İki yönlü bağlamanın aksine, UDF durum değişikliklerinin yalnızca açık eylemler (Intent/Event) aracılığıyla gerçekleşmesini garanti eder, bu da veri akışını öngörülebilir ve izlenebilir kılar. Google I/O 2024'e göre UDF, orta ve yüksek karmaşıklıkta iş mantığına sahip Jetpack Compose ve SwiftUI uygulamaları için önerilen mimaridir.
Önemli Noktalar
Tek Yönlü Veri Akışı (UDF), verilerin kapalı bir döngüde tek yönde hareket ettiği ve View ile Model arasındaki geri bildirim döngülerini ortadan kaldıran bir mimari desendir. UI'daki bir değişikliğin modeli hemen güncellediği İki Yönlü Bağlama'nın aksine, UDF her durum değişikliği için açık bir eylem (Intent, Event, Action) gerektirir. Bu, veri akışını tamamen öngörülebilir kılar: herhangi bir zamanda, hangi eylemin mevcut duruma yol açtığı belirlenebilir.
UDF kavramı web çerçevelerinden — Redux (JavaScript, 2015) ve Elm (2012) — gelmiştir ve mobil geliştirme için uyarlanmıştır. Google I/O 2024'e göre UDF, LiveData ile klasik MVVM'nin yerini alarak Jetpack Compose için önerilen mimari haline gelmiştir. iOS'ta, Point-Free tarafından The Composable Architecture (TCA)'da benzer bir yaklaşım uygulanmıştır ve Swift Community Survey (2024)'e göre iOS geliştiricilerinin %15'inden fazlası tarafından kullanılmaktadır.
UDF'nin ana avantajı Tek Bir Gerçek Kaynağıdır (SSOT): tüm uygulama durumu tek bir yerde depolanır ve kesin olarak tanımlanmış işlemlerle değiştirilir. Bu, hata ayıklamayı, test etmeyi ve hata yeniden üretimini basitleştirir, çünkü her durum değişikliği günlüğe kaydedilir ve aynı Intent'ler yeniden gönderilerek yeniden üretilebilir.
Temel UDF döngüsü dört adımdan oluşur: State (mevcut durum) View'da oluşturulur; kullanıcı bir eylem gerçekleştirir ve bu bir Intent haline gelir; Intent bir Reducer'da (saf fonksiyon) işlenir ve yeni bir State oluşturur; yeni durum yeniden oluşturma için View'a iletilir. Bu döngü her kullanıcı veya sistem olayında tekrarlanır.
Döngünün her bir öğesinin katı bir sorumluluğu vardır: State — belirli bir andaki ekran durumunu tanımlayan değişmez bir nesne; View — State'i oluşturan bir fonksiyon; Intent — kullanıcının niyetini tanımlayan bir değer (örneğin, LoginIntent.Submit); Reducer — yan etkileri olmayan, mevcut State ve Intent'i alan ve yeni bir State döndüren saf bir fonksiyon. Yan etkiler (ağ istekleri, veritabanı işlemleri) ayrı bir Middleware veya Effect katmanına taşınır.
Google Android Architecture makalesine (2024) göre, Reducer'ın saflığı önemli bir gerekliliktir: Bir Reducer bir ağ çağrısı veya veritabanı yazma işlemi içeriyorsa, veri akışını test etmek ve hata ayıklamak imkansız hale gelir. Tüm yan etkiler, Reducer'ı çağırmadan önce bir ViewModel coroutine'i veya Swift Task'ında yürütülmeli ve sonuç yeni bir Intent olarak gönderilmelidir.
Android'de UDF uygulaması üç Jetpack bileşeni üzerine kurulmuştur: ViewModel yaşam döngüsünü yönetir, StateFlow reaktif bir durum akışı sağlar, Intent (sealed class) olası tüm kullanıcı eylemlerini tanımlar. View, Compose'da collectAsState() veya View sisteminde observe() aracılığıyla StateFlow'a abone olur.
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) }
)
}Örnek, Android'deki tam UDF döngüsünü göstermektedir: LoginIntent olası tüm eylemleri (e-posta değişikliği, şifre değişikliği, form gönderimi) tanımlar, LoginState değişmez durumdur, LoginViewModel Intent'leri işler ve StateFlow'u günceller ve Compose ekranı collectAsState() aracılığıyla duruma abone olur. Her durum değişikliği, belirli bir Intent'in işlenmesinin sonucudur ve bu da veri akışını tamamen şeffaf hale getirir.
iOS'ta UDF, The Composable Architecture (TCA) (Point-Free) veya iOS 17+ ile yerel Observable deseni aracılığıyla uygulanır. TCA, State + Action + Reducer + Store'un hazır bir döngüsünü sağlar; burada Store tek gerçek kaynağıdır ve View, @Observable veya ObservableObject aracılığıyla değişikliklere abone olur.
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("Giriş Yap") { viewStore.send(.submit) }
}
}
}
}loginReducer saf bir fonksiyondur: ağ isteklerini doğrudan gerçekleştirmez, bunun yerine TCA çalışma zamanı tarafından yürütülecek bir Effect döndürür. Bu, testlerde efektleri değiştirerek indirgeyicinin izole bir şekilde test edilmesine olanak tanır. View, WithViewStore aracılığıyla Store değişikliklerine abone olur ve send() aracılığıyla Actions gönderir. TCA, Store yok edildiğinde efektlerin iptalini otomatik olarak yöneterek bellek sızıntılarını önler.
MVVM ve UDF sıklıkla karıştırılır, ancak aralarında temel bir fark vardır. MVVM, kodu üç katmana (Model, View, ViewModel) ayıran yapısal bir desendir ancak veri akışının yönünü tanımlamaz. UDF, verilerin bu yapı içinde nasıl hareket ettiğini tanımlayan bir davranış desenidir. LiveData ile MVVM'de hem iki yönlü bağlama hem de tek yönlü akış mümkündür — UDF, MVVM'ye katı Intent işleme kuralları ekler.
Android Developers belgelerine (2024) göre, Compose için önerilen mimari MVVM içinde UDF'dir: ViewModel State'i depolar ve Intent'leri işler, View State'e abone olur ve Intent'leri gönderir. Google, DataBinding aracılığıyla İki Yönlü Bağlama ile klasik MVVM'yi yalnızca iş mantığı olmayan basit ekranlar için önermektedir. Jetpack Compose için birincil senaryo, açık olay işleme ile UDF'dir.
Karşılaştırma tablosu:
| Özellik | MVVM (klasik) | MVVM + UDF |
|---|---|---|
| Veri akışı | Tanımlı değil | Kesinlikle tek yönlü |
| Durum değişikliği | Doğrudan setText() ile | Yalnızca Intent → Reducer ile |
| Tek Bir Gerçek Kaynağı | Hayır | Evet |
| Reducer test edilebilirliği | Düşük | Yüksek (saf fonksiyon) |
| Google önerisi | Eski yaklaşım | Compose için birincil |
En yaygın hata Reducer içindeki yan etkilerdir. MVVM'ye alışkın geliştiriciler ağ isteklerini doğrudan Intent işleyicisine yerleştirir, bu da Reducer'ı saf olmayan hale getirir ve test edilebilirliği bozar. Tüm efektler bir değer (Effect / SideEffect) olarak döndürülmeli ve çerçeve altyapısı tarafından yürütülmelidir. Android'de bunun için ViewModel'de coroutine'ler kullanılır; TCA'da — Effect.run.
İkinci hata aşırı ayrıntılı Intent'lerdir. Her tuş vuruşu, kaydırıcı hareketi ve metin değişikliği ayrı bir Intent oluşturur. Giriş alanları için bu aşırıdır — bu gibi durumlarda, form içinde tek yönlü akışla Bağlama (yerel durum) kullanmak ve yalnızca önemli eylemler (gönderme, gezinme) için genel bir Intent göndermek kabul edilebilir.
Üçüncü hata efekt iptali işleme eksikliğidir. Kullanıcı ekrandan ayrıldığında bir coroutine veya Task hala yürütülüyorsa, sonuç zaten yok edilmiş bir View'a uygulanabilir. Android'de viewModelScope.cancel() veya takeWhileActive() kullanın; TCA'da efektler Store yok edildiğinde otomatik olarak iptal edilir. Google Issue Tracker (2024)'e göre, tamamlanmamış coroutine'lerden kaynaklanan sızıntılar Compose uygulamalarında ilk 5 çökme nedeni arasındadır.
Sıkça Sorulan Sorular
MVI (Model-View-Intent), üç zorunlu öğeye sahip UDF'nin belirli bir durumudur: Intent (niyet), Model (durum), View (görüntüleme). Temel fark, MVI'da her ekran durumunun tek bir değişmez yapı (Sealed class) ile tanımlanması ve View'ın Model'den UI'ya saf bir fonksiyon olmasıdır. UDF, Redux ve Elm dahil olmak üzere herhangi bir tek yönlü akışı tanımlayan daha geniş bir terimdir. Google belgelerinde UDF terimi genel bir ad olarak kullanılırken, MVI belirli bir uygulamadır.
UDF, doğrulamasız tek bir giriş alanına sahip ekranlar, statik sayfalar ve yer tutucu ekranlar için aşırıdır. Bir ekranın iş mantığı yoksa ve durumu kullanıcı eylemlerine bağlı değilse, UDF fayda sağlamadan gereksiz kod ekler. Bu tür senaryolar için tek yönlü bağlama veya SwiftUI'de basit @State yeterlidir. UDF, olası ekran durumu sayısı 3-4'ü aştığında ve/veya yan etkiler mevcut olduğunda haklı çıkar.
Reducer saf bir fonksiyon olduğundan, test etmek onu farklı State ve Intent kombinasyonlarıyla çağırmak ve sonuçta ortaya çıkan State ve Effect'i kontrol etmekten ibarettir. Android'de StateFlow'u test etmek için Turbine kullanın: bir Intent gönderin, sonraki State emisyonunu kontrol edin. TCA'da, bir Action'dan sonra yalnızca beklenen State alanlarının değiştiğini ve yalnızca beklenen Effect'lerin yürütüldüğünü otomatik olarak doğrulayan yerleşik bir TestStore vardır.
Evet, bunları birleştirmek kabul edilebilir ve genellikle en uygunudur. Bir form içindeki giriş alanları için, her tuş vuruşunda bir Intent oluşturmaktan kaçınmak için yerel İki Yönlü Bağlama (veya SwiftUI'de Binding) kullanın. Form gönderiminde, toplanan verilerle tek bir Intent gönderin ve bu Reducer tarafından işlenir. Bu hibrit yaklaşım — yerel İki Yönlü Bağlama ile genel UDF — ticari SwiftUI uygulamalarının %70'inde kullanılır (Swift Community Survey 2024 verileri).
Her üç desen de tek bir gerçek kaynağı ile tek yönlü veri akışını uygular. Elm (2012) — işlevsel bir dil — ilk olarak saf Model → View → Update döngüsünü tanıttı. Redux (2015), Store, Reducer ve Action kavramlarıyla Elm'i JavaScript'e uyarladı. UDF, bu fikirlerin mobil geliştirme için genelleştirilmesidir. Her üç yaklaşım da atomik durum güncellemeleri yoluyla değişikliklerin öngörülebilirliğini garanti eder.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun