31 Mei 2025·6 menit membaca

Kotlin MVI vs MVVM untuk Aplikasi Android yang Banyak Form: Keadaan UI

Kotlin MVI vs MVVM untuk aplikasi Android yang banyak form, dijelaskan dengan cara praktis memodelkan validasi, UI optimistis, status error, dan draf offline.

Kotlin MVI vs MVVM untuk Aplikasi Android yang Banyak Form: Keadaan UI

Mengapa aplikasi Android yang banyak form cepat berantakan

Aplikasi yang penuh form terasa lambat atau rapuh karena pengguna terus menunggu keputusan kecil yang harus dibuat kode Anda: apakah field ini valid, apakah penyimpanan berhasil, apakah kita perlu menampilkan error, dan apa yang terjadi kalau jaringan terputus.

Form juga paling cepat memamerkan bug state karena mereka mencampur beberapa jenis state sekaligus: state UI (apa yang terlihat), state input (apa yang diketik pengguna), state server (apa yang tersimpan), dan state sementara (apa yang sedang berjalan). Ketika itu tidak sinkron, aplikasi mulai terasa “acak”: tombol nonaktif pada waktu yang salah, error lama tetap muncul, atau layar reset setelah rotasi.

Sebagian besar masalah berkumpul di empat area: validasi (terutama aturan lintas-field), UI optimistis (umpan balik cepat sementara pekerjaan masih berjalan), penanganan error (gagal yang jelas dan bisa dipulihkan), dan draf offline (jangan sampai kehilangan pekerjaan yang belum selesai).

UX form yang baik mengikuti beberapa aturan sederhana:

  • Validasi harus berguna dan dekat dengan field. Jangan memblokir pengetikan. Tegas ketika perlu, biasanya saat submit.
  • UI optimistis harus langsung merefleksikan tindakan pengguna, tetapi juga perlu rollback bersih jika server menolak.
  • Error harus spesifik, dapat ditindaklanjuti, dan tidak pernah menghapus input pengguna.
  • Draf harus bertahan dari restart, gangguan, dan koneksi buruk.

Itulah kenapa perdebatan arsitektur jadi intens untuk form. Pola yang Anda pilih menentukan seberapa dapat diprediksi state-state tersebut dalam kondisi tekanan.

Ringkasan cepat: MVVM dan MVI dengan kata-kata sederhana

Perbedaan nyata antara MVVM dan MVI adalah bagaimana perubahan mengalir melalui sebuah layar.

MVVM (Model View ViewModel) biasanya seperti ini: ViewModel memegang data layar, mengeksposnya ke UI (sering lewat StateFlow atau LiveData), dan menyediakan metode seperti save, validate, atau load. UI memanggil fungsi ViewModel saat pengguna berinteraksi.

MVI (Model View Intent) biasanya seperti ini: UI mengirim event (intent), sebuah reducer memprosesnya, dan layar dirender dari satu objek state yang merepresentasikan semua yang UI butuhkan sekarang. Side effect (jaringan, database) dipicu secara terkontrol dan melaporkan hasil kembali sebagai event.

Cara mudah mengingat pola pikir:

  • MVVM bertanya, “Data apa yang harus diekspos ViewModel, dan metode apa yang harus ditawarkan?”
  • MVI bertanya, “Event apa yang bisa terjadi, dan bagaimana mereka mentransformasikan satu state ke state lain?”

Kedua pola bekerja baik untuk layar sederhana. Begitu Anda menambahkan validasi lintas-field, autosave, retry, dan draf offline, Anda butuh aturan lebih ketat tentang siapa yang bisa mengubah state dan kapan. MVI menegakkan aturan-aturan itu secara bawaan. MVVM masih bisa bekerja dengan baik, tapi perlu disiplin: jalur update konsisten dan penanganan peristiwa satu-kali (toast, navigasi) yang hati-hati.

Cara memodelkan state form tanpa kejutan

Cara tercepat kehilangan kontrol adalah membiarkan data form hidup di terlalu banyak tempat: binding view, banyak flow, dan "satu boolean lagi". Layar yang banyak form tetap dapat diprediksi ketika ada satu sumber kebenaran.

Bentuk praktis FormState

Usahakan satu FormState yang memegang input mentah plus beberapa flag turunan yang dapat Anda percaya. Buatlah sederhana dan lengkap, meski terasa agak besar.

data class FormState(
  val fields: Fields,
  val fieldErrors: Map<FieldId, String> = emptyMap(),
  val formError: String? = null,
  val isDirty: Boolean = false,
  val isValid: Boolean = false,
  val submitStatus: SubmitStatus = SubmitStatus.Idle,
  val draftStatus: DraftStatus = DraftStatus.NotSaved
)

sealed class SubmitStatus { object Idle; object Saving; object Saved; data class Failed(val msg: String) }
sealed class DraftStatus { object NotSaved; object Saving; object Saved }

Ini menjaga validasi per-field terpisah dari masalah tingkat form (seperti “total harus > 0”). Flag turunan seperti isDirty dan isValid sebaiknya dihitung di satu tempat, bukan diimplementasikan ulang di UI.

Model mental yang bersih: fields (apa yang diketik pengguna), validasi (apa yang salah), status (apa yang sedang dikerjakan aplikasi), kekotoran (apa yang berubah sejak terakhir disimpan), dan draf (apakah ada salinan offline).

Di mana efek satu-kali berada

Form juga memicu efek satu-kali: snackbar, navigasi, banner “tersimpan”. Jangan masukkan ini ke dalam FormState, atau mereka akan muncul lagi saat rotasi atau ketika UI re-subscribe.

Di MVVM, keluarkan efek melalui channel terpisah (mis. SharedFlow). Di MVI, modelkan mereka sebagai Effects (atau Events) yang dikonsumsi UI sekali. Pemisahan ini mencegah error “hantu” dan pesan sukses ganda.

Alur validasi di MVVM vs MVI

Validasi adalah tempat layar form mulai terasa rapuh. Pilihan kuncinya adalah di mana aturan berada dan bagaimana hasilnya kembali ke UI.

Aturan sinkron sederhana (field wajib, panjang minimal, rentang angka) sebaiknya dijalankan di ViewModel atau lapisan domain, bukan di UI. Itu menjaga aturan bisa dites dan konsisten.

Aturan asinkron (seperti “apakah email ini sudah terpakai?”) lebih rumit. Anda perlu menangani loading, hasil usang, dan kasus “pengguna mengetik lagi”.

Di MVVM, validasi sering menjadi campuran state dan metode pembantu: UI mengirim perubahan (update teks, perubahan fokus, klik submit) ke ViewModel; ViewModel memperbarui StateFlow/LiveData dan mengekspos error per-field serta canSubmit turunan. Pemeriksaan async biasanya memulai job, lalu memperbarui flag loading dan error ketika selesai.

Di MVI, validasi cenderung lebih eksplisit. Pembagian tanggung jawab praktis:

  • Reducer menjalankan validasi sinkron dan memperbarui field errors segera.
  • Sebuah effect menjalankan validasi async dan mengirim hasil sebagai intent.
  • Reducer menerapkan hasil itu hanya jika masih sesuai dengan input terbaru.

Langkah terakhir itu penting. Jika pengguna mengetik email baru saat pemeriksaan “unique email” berjalan, hasil lama tidak boleh menimpa input saat ini. MVI sering membuat ini lebih mudah dikodekan karena Anda bisa menyimpan nilai terakhir yang diperiksa dalam state dan mengabaikan respons usang.

UI optimistis dan penyimpanan async

Pindah dari form ke pembayaran
Jika form berujung pada pembayaran, tambahkan logika Stripe dalam alur yang sama.
Hubungkan Stripe

UI optimistis berarti layar bertindak seolah penyimpanan berhasil sebelum balasan jaringan datang. Di form, itu sering berarti tombol Save berubah menjadi “Saving…”, indikator kecil “Saved” muncul saat selesai, dan input tetap bisa digunakan (atau sengaja dikunci) sementara request sedang berjalan.

Di MVVM, ini biasa diimplementasikan dengan mengganti flag seperti isSaving, lastSavedAt, dan saveError. Risikonya adalah drift: penyimpanan yang tumpang tindih bisa membuat flag-flag itu tidak konsisten. Di MVI, reducer memperbarui satu objek state, sehingga “Saving” dan “Disabled” cenderung tidak saling kontradiksi.

Untuk menghindari double submit dan kondisi race, perlakukan setiap penyimpanan sebagai event teridentifikasi. Jika pengguna mengetuk Save dua kali atau mengedit saat menyimpan, Anda butuh aturan untuk respons mana yang menang. Beberapa langkah pencegahan bekerja untuk kedua pola: nonaktifkan Save saat menyimpan (atau debounce tap), lampirkan requestId (atau versi) pada setiap save dan abaikan respons usang, batalkan pekerjaan yang sedang berjalan saat pengguna meninggalkan layar, dan tentukan apa arti edit selama save (antri save lain, atau tandai form sebagai dirty lagi).

Partial success juga umum: server menerima beberapa field tapi menolak yang lain. Modelkan itu secara eksplisit. Simpan error per-field (dan, bila perlu, status sinkron per-field) sehingga Anda bisa menampilkan “Saved” keseluruhan sambil menyoroti field yang membutuhkan perhatian.

Status error yang bisa dipulihkan pengguna

Go live dengan cara Anda
Deploy ke AppMaster Cloud atau ke AWS, Azure, atau Google Cloud Anda sendiri.
Deploy Sekarang

Layar form gagal dengan lebih banyak cara daripada hanya “terjadi kesalahan”. Jika setiap kegagalan menjadi toast generik, pengguna akan mengetik ulang data, kehilangan kepercayaan, dan meninggalkan alur. Tujuannya selalu sama: amankan input, tunjukkan perbaikan yang jelas, dan buat retry terasa normal.

Membantu jika memisahkan error berdasarkan tempatnya. Email format salah tidak sama dengan outage server.

Error field harus inline dan terikat pada satu input. Error tingkat form harus dekat aksi submit dan menjelaskan apa yang menghalangi pengiriman. Error jaringan harus menawarkan retry dan menjaga form tetap bisa diedit. Error izin atau autentikasi harus memandu pengguna untuk melakukan re-auth sambil mempertahankan draf.

Aturan pemulihan inti: jangan pernah menghapus input pengguna saat gagal. Jika penyimpanan gagal, pertahankan nilai saat ini di memori dan di disk. Retry harus mengirim ulang payload yang sama kecuali pengguna mengedit.

Perbedaan pola adalah bagaimana error server dipetakan kembali ke UI. Di MVVM, mudah untuk memperbarui banyak flow atau field dan tidak sengaja menciptakan inkonsistensi. Di MVI, Anda biasanya menerapkan respons server dalam satu langkah reducer yang memperbarui fieldErrors dan formError bersama-sama.

Juga putuskan apa yang termasuk state vs efek satu-kali. Error inline dan “submission failed” termasuk state (mereka harus bertahan saat rotasi). Aksi satu-kali seperti snackbar, getar, atau navigasi harus menjadi efek.

Draf offline dan pemulihan form yang sedang berlangsung

Aplikasi yang banyak form terasa “offline” bahkan saat jaringan baik. Pengguna berpindah aplikasi, OS membunuh proses Anda, atau sinyal hilang di tengah langkah. Draf mencegah mereka mengulang dari awal.

Pertama, definisikan apa arti sebuah draf. Menyimpan hanya model “bersih” seringkali tidak cukup. Anda biasanya ingin memulihkan layar persis seperti sebelumnya, termasuk field yang baru diketik setengah jadi.

Yang layak dipersist adalah sebagian besar input mentah pengguna (string seperti diketik, ID terpilih, URI lampiran), plus metadata cukup untuk menggabungkan dengan aman nanti: snapshot server terakhir dan penanda versi (updatedAt, ETag, atau increment sederhana). Validasi bisa dihitung ulang saat restore.

Pilihan penyimpanan tergantung sensitivitas dan ukuran. Draf kecil bisa di preferences, tapi form multi-langkah dan lampiran lebih aman di database lokal. Jika draf berisi data pribadi, gunakan penyimpanan terenkripsi.

Pertanyaan arsitektural terbesar adalah di mana sumber kebenaran berada. Di MVVM, tim sering menyimpan dari ViewModel setiap kali field berubah. Di MVI, menyimpan setelah setiap update reducer bisa lebih sederhana karena Anda menyimpan satu state koheren (atau objek Draft turunan).

Waktu autosave penting. Menyimpan pada setiap ketukan berisik; debounce pendek (mis. 300–800 ms) plus simpan saat pindah langkah bekerja baik.

Saat pengguna kembali online, Anda butuh aturan merge. Pendekatan praktis: jika versi server tidak berubah, terapkan draf dan submit. Jika berubah, tampilkan pilihan jelas: pertahankan draf saya atau muat ulang data server.

Langkah demi langkah: implementasikan form andal dengan salah satu pola

Kirim backend bersamaan
Buat endpoint API dan logika bisnis tanpa menulis boilerplate untuk setiap form.
Hasilkan Backend

Form andal dimulai dari aturan yang jelas, bukan kode UI. Setiap aksi pengguna harus mengarah ke state yang dapat diprediksi, dan setiap hasil async harus punya satu tempat jelas untuk mendarat.

Tuliskan tindakan yang harus ditangani layar Anda: mengetik, kehilangan fokus, submit, retry, dan navigasi langkah. Di MVVM ini menjadi metode ViewModel dan update state. Di MVI ini menjadi intent eksplisit.

Kemudian bangun secara bertahap:

  1. Definisikan event untuk siklus hidup lengkap: edit, blur, submit, save success/failure, retry, restore draft.
  2. Desain satu objek state: nilai field, error per-field, status form keseluruhan, dan “ada perubahan yang belum disimpan”.
  3. Tambahkan validasi: cek ringan saat edit, cek lebih berat saat submit.
  4. Tambahkan aturan save optimistis: apa yang berubah segera, dan apa yang memicu rollback.
  5. Tambahkan draf: autosave dengan debounce, restore saat buka, dan tampilkan indikator kecil “draf dipulihkan” agar pengguna percaya pada apa yang mereka lihat.

Perlakukan error sebagai bagian dari pengalaman. Pertahankan input, sorot hanya yang perlu diperbaiki, dan tawarkan satu aksi selanjutnya yang jelas (edit, retry, atau pertahankan draf).

Jika Anda ingin mem-prototype state form kompleks sebelum menulis UI Android, platform no-code seperti AppMaster dapat berguna untuk memvalidasi alur terlebih dahulu. Setelah itu Anda bisa mengimplementasikan aturan yang sama di MVVM atau MVI dengan kejutan lebih sedikit.

Contoh skenario: form laporan pengeluaran multi-langkah

Bayangkan form laporan pengeluaran 4-langkah: details (tanggal, kategori, jumlah), upload struk, catatan, lalu review dan submit. Setelah submit, tampilkan status approval seperti Draft, Submitted, Rejected, Approved. Bagian rumitnya adalah validasi, penyimpanan yang bisa gagal, dan mempertahankan draf saat ponsel offline.

Di MVVM, Anda biasanya menyimpan FormUiState di ViewModel (sering StateFlow). Setiap perubahan field memanggil fungsi ViewModel seperti onAmountChanged() atau onReceiptSelected(). Validasi berjalan saat perubahan, saat navigasi langkah, atau saat submit. Struktur umum adalah input mentah plus error per-field, dengan flag turunan yang mengontrol apakah Next/Submit aktif.

Di MVI, alur yang sama menjadi eksplisit: UI mengirim intent seperti AmountChanged, NextClicked, SubmitClicked, dan RetrySave. Reducer mengembalikan state baru. Side effect (upload receipt, panggil API, tampilkan snackbar) dijalankan di luar reducer dan memberi balik hasil sebagai event.

Dalam praktiknya, MVVM memudahkan menambahkan fungsi dan memperbarui flow dengan cepat. MVI membuatnya lebih sulit untuk melewati transisi state secara tidak sengaja karena setiap perubahan disalurkan melalui reducer.

Kesalahan umum dan jebakan

Dukung form dengan alat admin
Buat alat internal untuk meninjau pengiriman, memperbaiki error, dan mendukung retry.
Buat Panel Admin

Sebagian besar bug form muncul dari aturan yang tidak jelas tentang siapa yang memiliki kebenaran, kapan validasi dijalankan, dan apa yang terjadi ketika hasil async datang terlambat.

Kesalahan paling umum adalah mencampur sumber kebenaran. Jika sebuah text field kadang membaca dari widget, kadang dari ViewModel state, dan kadang dari draf yang di-restore, Anda akan mendapatkan reset acak dan laporan “input saya hilang”. Pilih satu state kanonik untuk layar dan turunkan semuanya dari sana (domain model, baris cache, payload API).

Jebakan mudah lainnya adalah membingungkan state dengan event. Toast, navigasi, atau banner “Saved!” adalah satu-kali. Pesan error yang harus tetap terlihat sampai pengguna mengedit adalah state. Mencampur ini menyebabkan efek ganda saat rotasi atau feedback yang hilang.

Dua isu correctness yang sering muncul:

  • Over-validating pada setiap ketukan, terutama untuk cek yang mahal. Debounce, validasi pada blur, atau validasi hanya field yang disentuh.
  • Mengabaikan hasil async yang tidak berurutan. Jika pengguna menyimpan dua kali atau mengedit setelah penyimpanan, respons lama dapat menimpa input baru kecuali Anda menggunakan request IDs (atau logika "hanya yang terbaru")

Terakhir, draf bukan sekadar “simpan JSON”. Tanpa versioning, pembaruan aplikasi dapat merusak restore. Tambahkan versi skema sederhana dan cerita migrasi, bahkan jika itu berarti “hapus dan mulai ulang” untuk draf sangat lama.

Checklist cepat sebelum rilis

Validasi UX sebelum kode Kotlin
Validasi UI dan alur kerja sebelum menulis kode Kotlin native.
Buat Aplikasi Mobile

Sebelum berdebat MVVM vs MVI, pastikan form Anda punya satu sumber kebenaran yang jelas. Jika sebuah nilai bisa berubah di layar, ia harus berada di state, bukan di widget view atau flag tersembunyi.

Pemeriksaan praktis sebelum rilis:

  • State mencakup input, error per-field, status save (idle/saving/saved/failed), dan status draf/queue sehingga UI tidak perlu menebak.
  • Aturan validasi murni dan bisa dites tanpa UI.
  • UI optimistis punya jalur rollback untuk penolakan server.
  • Error tidak pernah menghapus input pengguna.
  • Pemulihan draf dapat diprediksi: banner auto-restore yang jelas atau aksi "Restore draft" eksplisit.

Satu tes lagi yang menangkap bug nyata: nyalakan mode pesawat saat penyimpanan berlangsung, matikan, lalu coba retry dua kali. Retry kedua tidak boleh membuat duplikat. Gunakan request ID, idempotency key, atau penanda "pending save" lokal agar retry aman.

Jika jawaban Anda kabur, perbaiki model state dulu, lalu pilih pola yang membuat aturan itu mudah ditegakkan.

Langkah berikutnya: memilih jalur dan membangun lebih cepat

Mulailah dengan satu pertanyaan: seberapa mahal kalau form Anda berakhir dalam keadaan setengah-terupdate? Jika biayanya rendah, tetap sederhana.

MVVM cocok ketika layar sederhana, state sebagian besar “fields + errors”, dan tim Anda sudah nyaman mengirim dengan ViewModel + LiveData/StateFlow.

MVI lebih cocok ketika Anda butuh transisi state yang ketat dan dapat diprediksi, banyak event async (autosave, retry, sinkron), atau ketika bug mahal (pembayaran, kepatuhan, alur kritis).

Mana pun jalur yang Anda pilih, tes dengan return tertinggi untuk form biasanya tidak menyentuh UI: edge case validasi, transisi state (edit, submit, success, failure, retry), rollback save optimistis, dan restore draf plus perilaku konflik.

Jika Anda juga membutuhkan backend, layar admin, dan API bersama aplikasi mobile, AppMaster (appmaster.io) dapat menghasilkan backend produksi, web, dan app native dari satu model, yang membantu menjaga aturan validasi dan alur kerja konsisten di seluruh permukaan.

FAQ

When should I choose MVVM vs MVI for a form-heavy Android screen?

Pilih MVVM ketika alur form Anda relatif linier dan tim Anda sudah punya konvensi kuat untuk StateFlow/LiveData, peristiwa satu-kali, dan pembatalan. Pilih MVI ketika Anda mengantisipasi banyak pekerjaan async yang saling tumpang tindih (autosave, retry, upload) dan Anda ingin aturan yang ketat sehingga perubahan state tidak bisa “masuk diam-diam” dari banyak tempat.

What’s the simplest way to keep form state from drifting out of sync?

Mulai dari satu objek state layar (mis. FormState) yang berisi nilai field mentah, error per-field, error tingkat form, dan status jelas seperti Saving atau Failed. Hitung flag turunan seperti isValid dan canSubmit di satu tempat sehingga UI hanya merender dan tidak mengambil keputusan logika lagi.

How often should validation run in a form: on every keystroke or only on submit?

Jalankan pemeriksaan ringan dan murah saat pengguna mengedit (wajib, rentang, format dasar), dan jalankan pemeriksaan ketat saat submit. Letakkan logika validasi di luar UI agar dapat dites, dan simpan error di state agar bertahan saat rotasi atau pemulihan proses.

How do I handle async validation like “email already taken” without stale results?

Anggap validasi async seperti “input terakhir menang.” Simpan nilai yang divalidasi (atau request/version id) dan abaikan hasil yang tidak cocok dengan state saat ini. Ini mencegah respons usang menimpa ketikan terbaru, sumber umum dari pesan error “acak”.

What’s a safe default approach for optimistic UI when saving a form?

Perbarui UI segera untuk merefleksikan aksi (mis. tampilkan Saving… dan biarkan input terlihat), tetapi selalu sediakan jalur rollback jika server menolak. Gunakan request id/version, nonaktifkan atau debounce tombol Save, dan tentukan arti pengeditan saat proses penyimpanan berlangsung (kunci field, antri penyimpanan lain, atau tandai sebagai dirty lagi).

How should I structure error states so users can recover without retyping?

Jangan pernah menghapus input pengguna saat gagal. Tampilkan masalah per-field secara inline pada field terkait, tempatkan error tingkat form dekat aksi submit, dan buat kegagalan jaringan dapat dipulihkan dengan retry yang mengirimkan payload yang sama kecuali pengguna mengubahnya.

Where should one-time events like snackbars and navigation live?

Jauhkan efek satu-kali dari state persisten. Di MVVM, kirim lewat stream terpisah (mis. SharedFlow), dan di MVI, modelkan sebagai Effects yang dikonsumsi UI sekali. Ini menghindari snackbar ganda atau navigasi berulang setelah rotasi atau re-subscribe.

What exactly should I save for offline drafts of a form?

Simpan mayoritas input mentah pengguna (teks tak selesai, ID terpilih, URI lampiran), plus metadata minimal untuk pemulihan dan penggabungan aman nanti, seperti penanda versi server terakhir. Hitung ulang validasi saat restore daripada menyimpannya, dan sertakan versi skema sederhana untuk migrasi saat update aplikasi.

How should autosave be timed so it feels reliable but not noisy?

Gunakan debounce pendek (beberapa ratus milidetik) ditambah simpan pada perubahan langkah atau saat aplikasi di-background. Menyimpan pada setiap ketukan terlalu bising; menyimpan hanya saat keluar berisiko kehilangan kerja ketika proses mati.

How do I handle draft conflicts when the server data changed while the user was offline?

Simpan penanda versi (mis. updatedAt, ETag, atau increment lokal) untuk snapshot server dan draf. Jika versi server tidak berubah, terapkan draf dan submit; jika berubah, tampilkan pilihan jelas: pertahankan draf saya atau muat ulang data server—jangan timpa diam-diam.

Mudah untuk memulai
Ciptakan sesuatu yang menakjubkan

Eksperimen dengan AppMaster dengan paket gratis.
Saat Anda siap, Anda dapat memilih langganan yang tepat.

Memulai