Pahami apa itu Unidirectional Data Flow — aliran data searah, pola arsitektur di mana data bergerak dalam siklus tertutup State → View → Intent → Reducer → State tanpa umpan balik. Berbeda dengan pengikatan dua arah, UDF menjamin bahwa perubahan status hanya terjadi melalui tindakan eksplisit (Intent/Event), yang membuat aliran data dapat diprediksi dan dilacak. Menurut Google I/O 2024, UDF adalah arsitektur yang direkomendasikan untuk aplikasi Jetpack Compose dan SwiftUI dengan logika bisnis tingkat menengah dan tinggi.
Poin Utama
Unidirectional Data Flow (UDF) — pola arsitektur di mana data bergerak dalam satu arah sepanjang siklus tertutup, mengecualikan umpan balik antara View dan Model. Berbeda dengan Two-Way Binding, di mana perubahan di UI segera memperbarui model, UDF memerlukan tindakan eksplisit (Intent, Event, Action) untuk setiap perubahan status. Ini membuat aliran data sepenuhnya dapat diprediksi: setiap saat dapat ditentukan tindakan mana yang menyebabkan status saat ini.
Konsep UDF berasal dari kerangka kerja web — Redux (JavaScript, 2015) dan Elm (2012) — dan diadaptasi untuk pengembangan seluler. Menurut Google I/O 2024, UDF telah menjadi arsitektur yang direkomendasikan untuk Jetpack Compose, menggantikan MVVM klasik dengan LiveData. Di iOS pendekatan serupa diimplementasikan dalam The Composable Architecture (TCA) dari Point-Free, yang digunakan oleh lebih dari 15% pengembang iOS menurut survei Swift Community (2024).
Keuntungan utama UDF — Single Source of Truth (SSOT): seluruh status aplikasi disimpan di satu tempat dan diubah melalui operasi yang ditentukan secara ketat. Ini menyederhanakan debugging, pengujian, dan reproduksi bug, karena setiap perubahan status dicatat dan dapat direproduksi dengan mengirim ulang Intent yang sama.
Siklus dasar UDF terdiri dari empat langkah: State (status saat ini) ditampilkan di View; pengguna melakukan tindakan yang diubah menjadi Intent (niat); Intent diproses di Reducer (fungsi murni), yang membuat State baru; status baru dikirimkan ke View untuk penggambaran ulang. Siklus ini berulang pada setiap peristiwa pengguna atau sistem.
Setiap elemen siklus memiliki tanggung jawab yang ketat: State — objek yang tidak dapat diubah (immutable) yang menggambarkan status layar pada saat tertentu; View — fungsi yang menampilkan State; Intent — nilai yang menggambarkan niat pengguna (misalnya, LoginIntent.Submit); Reducer — fungsi murni tanpa efek samping, yang menerima State dan Intent saat ini dan mengembalikan State baru. Efek samping (permintaan jaringan, basis data) dipindahkan ke lapisan Middleware atau Effect terpisah.
Menurut artikel Google Android Architecture (2024), kemurnian Reducer adalah persyaratan utama: jika Reducer berisi panggilan jaringan atau penulisan basis data, pengujian dan debugging aliran data menjadi tidak mungkin. Semua efek samping harus dijalankan di coroutine ViewModel atau Swift Task sebelum memanggil Reducer, dan hasilnya dikirim sebagai Intent baru.
Di Android implementasi UDF didasarkan pada tiga komponen Jetpack: ViewModel mengelola siklus hidup, StateFlow menyediakan aliran status reaktif, Intent (sealed class) menggambarkan semua tindakan pengguna yang mungkin. View berlangganan StateFlow melalui collectAsState() di Compose atau observe() di sistem View.
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) }
)
}Contoh ini menunjukkan siklus UDF lengkap di Android: LoginIntent menggambarkan semua tindakan yang mungkin (mengubah email, kata sandi, mengirim formulir), LoginState — status yang tidak dapat diubah, LoginViewModel memproses Intent dan memperbarui StateFlow, dan layar Compose berlangganan state melalui collectAsState(). Setiap perubahan status adalah hasil pemrosesan Intent tertentu, yang membuat aliran data sepenuhnya transparan.
Di iOS UDF diimplementasikan melalui The Composable Architecture (TCA) dari Point-Free atau pola Observable asli dengan iOS 17+. TCA menyediakan siklus siap pakai State + Action + Reducer + Store, di mana Store adalah satu-satunya sumber kebenaran, dan View berlangganan perubahan melalui @Observable atau ObservableObject.
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("Masuk") { viewStore.send(.submit) }
}
}
}
}Reducer loginReducer — fungsi murni: tidak menjalankan permintaan jaringan secara langsung, tetapi mengembalikan Effect yang akan dijalankan oleh lingkungan TCA. Ini memungkinkan pengujian reducer secara terisolasi, mengganti efek dalam pengujian. View melalui WithViewStore berlangganan perubahan Store dan mengirim Action melalui send(). TCA secara otomatis menangani pembatalan efek saat Store dihancurkan, mencegah kebocoran memori.
MVVM dan UDF sering tertukar, tetapi ada perbedaan mendasar di antara keduanya. MVVM adalah pola struktural yang membagi kode menjadi tiga lapisan (Model, View, ViewModel), tetapi tidak menentukan arah aliran data. UDF adalah pola perilaku yang menggambarkan bagaimana data bergerak di dalam struktur ini. Dalam MVVM dengan LiveData dapat terdapat pengikatan dua arah maupun aliran searah — UDF menambahkan aturan ketat pemrosesan Intent ke MVVM.
Menurut dokumentasi Android Developers (2024), arsitektur yang direkomendasikan untuk Compose adalah UDF di dalam MVVM: ViewModel menyimpan State dan memproses Intent, View berlangganan State dan mengirim Intent. MVVM klasik dengan Two-Way Binding melalui DataBinding direkomendasikan Google hanya untuk layar sederhana tanpa logika bisnis. Untuk Jetpack Compose skenario utamanya adalah UDF dengan pemrosesan peristiwa eksplisit.
Tabel perbandingan:
| Karakteristik | MVVM (klasik) | MVVM + UDF |
|---|---|---|
| Aliran data | Tidak ditentukan | Ketat searah |
| Perubahan status | Langsung melalui setText() | Hanya melalui Intent → Reducer |
| Single Source of Truth | Tidak | Ya |
| Kemampuan uji reducer | Rendah | Tinggi (fungsi murni) |
| Rekomendasi Google | Pendekatan usang | Utama untuk Compose |
Kesalahan paling umum — efek samping di dalam Reducer. Pengembang yang terbiasa dengan MVVM menempatkan permintaan jaringan langsung di handler Intent, yang membuat Reducer menjadi fungsi tidak murni dan merusak kemampuan uji. Semua efek harus dikembalikan sebagai nilai (Effect / SideEffect) dan dijalankan oleh infrastruktur kerangka kerja. Di Android digunakan coroutine di ViewModel, di TCA — Effect.run.
Kesalahan kedua — Intent yang terlalu detail. Setiap penekanan tombol, pergerakan penggeser, dan perubahan teks menghasilkan Intent terpisah. Untuk bidang masukan ini berlebihan — dalam kasus seperti itu, menggunakan Binding dengan aliran searah di dalam formulir (status lokal) dapat diterima, dan Intent global hanya dikirim pada tindakan signifikan (submit, navigasi).
Kesalahan ketiga — tidak ada penanganan pembatalan efek. Jika pengguna meninggalkan layar dan coroutine atau Task terus berjalan, hasilnya dapat diterapkan ke View yang sudah dihancurkan. Di Android gunakan viewModelScope.cancel() atau takeWhileActive(); di TCA efek otomatis dibatalkan saat Store dihancurkan. Menurut Google Issue Tracker (2024), kebocoran dari coroutine yang belum selesai termasuk dalam 5 penyebab utama kerusakan aplikasi Compose.
Pertanyaan yang Sering Diajukan
MVI (Model-View-Intent) — kasus khusus UDF dengan tiga elemen wajib: Intent (niat), Model (status), View (tampilan). Perbedaan utama adalah bahwa dalam MVI setiap status layar digambarkan oleh satu struktur yang tidak dapat diubah (Sealed class), dan View adalah fungsi murni dari Model ke UI. UDF adalah istilah yang lebih luas yang menggambarkan aliran searah apa pun, termasuk Redux dan Elm. Dalam dokumentasi Google istilah UDF digunakan sebagai nama umum, dan MVI sebagai implementasi spesifik.
UDF berlebihan untuk layar dengan satu bidang masukan tanpa validasi, halaman statis, dan layar placeholder. Jika layar tidak memiliki logika bisnis dan statusnya tidak bergantung pada tindakan pengguna, UDF menambahkan kode yang tidak perlu tanpa manfaat. Untuk skenario seperti itu, pengikatan searah sederhana atau @State di SwiftUI sudah cukup. UDF dibenarkan ketika jumlah kemungkinan status layar melebihi 3–4 dan/atau ada efek samping.
Karena Reducer adalah fungsi murni, pengujiannya dilakukan dengan memanggilnya dengan berbagai kombinasi State dan Intent serta memeriksa State dan Effect yang dihasilkan. Di Android gunakan Turbine untuk menguji StateFlow: kirim Intent, periksa emisi State berikutnya. Di TCA ada TestStore bawaan yang secara otomatis memeriksa apakah setelah Action hanya bidang State yang diharapkan berubah dan hanya Effect yang diharapkan dijalankan.
Ya, penggabungan diperbolehkan dan seringkali optimal. Untuk bidang masukan di dalam formulir, gunakan Two-Way Binding lokal (atau Binding di SwiftUI) agar tidak membuat Intent untuk setiap penekanan tombol. Saat mengirim formulir, kirim satu Intent dengan data yang terkumpul, yang diproses oleh Reducer. Pendekatan hibrida ini — UDF global dengan Two-Way Binding lokal — digunakan di 70% aplikasi SwiftUI komersial (data Swift Community Survey 2024).
Ketiga pola tersebut mengimplementasikan aliran data searah dengan satu sumber kebenaran. Elm (2012) — bahasa fungsional yang pertama kali memperkenalkan siklus murni Model → View → Update. Redux (2015) mengadaptasi Elm untuk JavaScript dengan konsep Store, Reducer, dan Action. UDF adalah generalisasi dari ide-ide ini untuk pengembangan seluler. Ketiga pendekatan menjamin prediktabilitas perubahan melalui pembaruan status atomik (tidak dapat dibagi).
Kesimpulan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga