Kotlin MVI बनाम MVVM फॉर्म-भारी Android ऐप्स के लिए: UI स्टेट्स
फॉर्म-भारी Android ऐप्स के लिए Kotlin MVI बनाम MVVM: वैलिडेशन, optimistic UI, एरर स्टेट्स और ऑफ़लाइन ड्राफ्ट कैसे मॉडल करें, व्यावहारिक तरीके के साथ समझाया गया।

क्यों फॉर्म-भारी Android ऐप्स जल्दी गड़बड़ हो जाते हैं
फॉर्म-भारी ऐप स्लो या नाज़ुक महसूस होते हैं क्योंकि यूज़र लगातार उन छोटे निर्णयों का इंतज़ार कर रहा होता है जो आपका कोड लेता है: क्या यह फील्ड वैलिड है, क्या सेव काम किया, क्या हमें एरर दिखाना चाहिए, और नेटवर्क कटने पर क्या होता है।
फॉर्म्स पहले ही स्टेट बग्स उजागर करते हैं क्योंकि वे एक साथ कई तरह की स्टेट मिक्स करते हैं: UI स्टेट (क्या दिख रहा है), इनपुट स्टेट (यूज़र ने क्या टाइप किया), सर्वर स्टेट (क्या सेव है), और अस्थायी स्टेट (क्या प्रोग्रेस में है)। जब ये सिन्क से बहार चले जाते हैं, तो ऐप “रैंडम” लगने लगता है: बटन गलत समय पर डिसेबल हो जाते हैं, पुराने एरर चिपक जाते हैं, या स्क्रीन रोटेशन पर रीसेट हो जाती है।
अधिकांश समस्याएँ चार क्षेत्रों में गुटबँधी करती हैं: वैलिडेशन (खासकर क्रॉस-फील्ड नियम), optimistic UI (तेज़ फीडबैक जबकि काम चल रहा है), एरर हैंडलिंग (स्पष्ट, रिकवर करने योग्य फेल्यर), और ऑफ़लाइन ड्राफ्ट (नाम पूरा न खोना)।
अच्छा फॉर्म UX कुछ सरल नियमों का पालन करता है:
- वैलिडेशन मददगार और फील्ड के पास होना चाहिए। टाइपिंग ब्लॉक न करें। ज़रूरत पड़ने पर, आमतौर पर सबमिट पर सख़्ती बरतें।
- Optimistic UI तुरंत यूज़र के एक्शन को दिखाना चाहिए, लेकिन सर्वर रिजेक्ट होने पर साफ़ rollback होना चाहिए।
- एरर स्पेसिफिक और actionable होने चाहिए, और कभी यूज़र का इनपुट मिटना नहीं चाहिए।
- ड्राफ्ट रिस्टार्ट, इंटरप्शन और खराब कनेक्शन से सुरक्षित रहना चाहिए।
इसीलिए आर्किटेक्चर पर बहसें तीव्र हो जाती हैं। जिस पैटर्न को आप चुनते हैं वह तय करता है कि दबाव में वे स्टेट्स कितने प्रेडिक्टेबल लगेंगे।
त्वरित रिफ्रेशर: साधारण शब्दों में MVVM और MVI
MVVM और MVI के बीच असली फर्क यह है कि स्क्रीन में परिवर्तन कैसे बहता है।
MVVM (Model View ViewModel) आमतौर पर इस तरह दिखता है: ViewModel स्क्रीन डेटा रखता है, UI को एक्सपोज़ करता है (अक्सर StateFlow या LiveData के ज़रिए), और save, validate, या load जैसे मेथड प्रदान करता है। UI यूज़र इंटरैक्शन पर ViewModel फंक्शन कॉल करता है।
MVI (Model View Intent) आमतौर पर इस तरह दिखता है: UI इवेंट्स (intents) भेजता है, एक reducer उन्हें प्रोसेस करता है, और स्क्रीन एक state ऑब्जेक्ट से रेंडर होती है जो अभी UI को चाहिए सब कुछ बताता है। साइड-इफेक्ट्स (नेटवर्क, DB) नियंत्रित तरीके से ट्रिगर होते हैं और परिणाम इवेंट्स के रूप में वापस रिपोर्ट करते हैं।
एक सरल याद रखने का तरीका:
- MVVM पूछता है, “ViewModel को कौन सा डेटा एक्सपोज़ करना चाहिए, और कौन-कौन से मेथड होने चाहिए?”
- MVI पूछता है, “कौन-कौन से इवेंट हो सकते हैं, और वे एक स्टेट को कैसे बदलते हैं?”
साधारण स्क्रीन के लिए दोनों पैटर्न ठीक काम करते हैं। जब आप क्रॉस-फील्ड वैलिडेशन, ऑटोसेव, रिट्राइस, और ऑफ़लाइन ड्राफ्ट जोड़ते हैं, तो यह ज़रूरी हो जाता है कि कौन कब स्टेट बदल सकता है उस पर सख़्त नियम हों। MVI डिफ़ॉल्ट रूप से उन नियमों को लागू करता है। MVVM अभी भी अच्छी तरह काम कर सकता है, लेकिन इसके लिए अनुशासन चाहिए: सुसंगत अपडेट पाथ और वन-ऑफ UI इवेंट्स (टूस्ट, नेविगेशन) का सावधानीपूर्वक हैंडलिंग।
बिना सरप्राइज़ के फॉर्म स्टेट कैसे मॉडल करें
सबसे तेज़ तरीका नियंत्रण खोने का यह है कि फॉर्म डेटा बहुत कई जगहों पर रहे: view bindings, कई flows, और "एक और boolean"। फॉर्म-भारी स्क्रीन तब ही प्रेडिक्टेबल रहते हैं जब एक ही सोर्स ऑफ ट्रूथ हो।
एक व्यावहारिक FormState आकार
एक सिंगल FormState के लिए लक्ष्य रखें जो रॉ इनपुट्स और कुछ derived flags रखता हो जिन पर आप भरोसा कर सकें। इसे बोरिंग और पूरा रखें, भले ही यह थोड़ा बड़ा लगे।
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 }
यह फील्ड-लेवल वैलिडेशन (प्रति इनपुट) को फॉर्म-लेवल समस्याओं (जैसे “total must be > 0”) से अलग रखता है। Derived flags जैसे isDirty और isValid एक ही जगह पर कम्प्यूट किए जाने चाहिए, UI में दोबारा लॉजिक न हो।
एक साफ़ मानसिक मॉडल यह है: fields (यूज़र ने क्या टाइप किया), validation (क्या ग़लत है), status (ऐप क्या कर रहा है), dirtiness (पिछली सेव के बाद क्या बदला), और drafts (क्या ऑफ़लाइन कॉपी मौजूद है)।
वन-ऑफ इफेक्ट्स कहाँ होने चाहिए
फॉर्म कई वन-टाइम इवेंट ट्रिगर करते हैं: snackbars, नेविगेशन, “saved” बैनर। इन्हें FormState के अंदर न रखें, वरना रोटेशन या UI के फिर से सब्स्क्राइब होने पर ये फिर से फायर हो जाएंगे।
MVVM में, इन्हें एक अलग चैनल (उदाहरण के लिए, SharedFlow) के माध्यम से इमिट करें। MVI में, इन्हें Effects (या Events) के रूप में मॉडल करें जिन्हें UI एक बार consume करे। यह अलगाव “फैंटम” एरर और duplicative success messages रोकता है।
MVVM बनाम MVI में वैलिडेशन फ्लो
वैलिडेशन वह जगह है जहाँ फॉर्म स्क्रीन कमजोर महसूस करने लगती हैं। मुख्य विकल्प यह है कि नियम कहाँ रहते हैं और परिणाम UI तक कैसे लौटते हैं।
सरल, synchronous नियम (required fields, min length, नंबर रेंज) ViewModel या domain लेयर में चलते हैं, UI में नहीं। इससे नियम टेस्टेबल और consistent रहते हैं।
असिंक्रोनस नियम (जैसे "क्या यह ईमेल पहले से लिया गया है?") जटिल होते हैं। आपको लोडिंग, stale परिणाम, और “यूज़र ने फिर से टाइप किया” केस से निपटना होता है।
MVVM में, वैलिडेशन अक्सर state और हेल्पर मेथड्स का मिश्रण बन जाती है: UI changes (text updates, focus changes, submit clicks) को ViewModel को भेजता है; ViewModel एक StateFlow/LiveData अपडेट करता है और प्रति-फील्ड एरर और एक derived “canSubmit” एक्सपोज़ करता है। Async चेक आमतौर पर एक job शुरू करते हैं, फिर लोडिंग फ्लैग और एरर अपडेट करते हैं जब यह पूरा होता है।
MVI में, वैलिडेशन अधिक explicit हो जाती है। एक व्यावहारिक जिम्मेदारी विभाजन:
- reducer sync validation चलाता है और फील्ड एरर तुरंत अपडेट करता है।
- एक effect async validation चलाता है और परिणाम intent भेजता है।
- reducer वह परिणाम तभी लागू करता है जब वह अभी के इनपुट से मेल खाता हो।
यह आखिरी कदम मायने रखता है। अगर यूज़र ने उस दौरान नया ईमेल टाइप कर दिया, जबकि "unique email" चेक चल रहा था, तो पुराने परिणाम वर्तमान इनपुट को ओवरराइट नहीं करने चाहिए। MVI इसको encode करना अक्सर आसान बनाता है क्योंकि आप state में last-checked value स्टोर कर सकते हैं और stale responses को ignore कर सकते हैं।
Optimistic UI और असिंक्रोनस सेव्स
Optimistic UI का मतलब है कि स्क्रीन ऐसे व्यवहार करे जैसे सेव हो चुका हो, जबकि नेटवर्क रिप्लाई अभी आना बाकी हो। फॉर्म में इसका मतलब अक्सर होता है कि Save बटन “Saving...” दिखाने लगता है, एक छोटा “Saved” इंडिकेटर दिखे जब यह हो जाए, और इनपुट यूज़ेबल बने रहें (या इरादतन लॉक)।
MVVM में, यह आमतौर पर isSaving, lastSavedAt, और saveError जैसे फ्लैग्स टॉगल करके किया जाता है। जोखिम है drift का: ओवरलैपिंग सेव्स उन फ्लैग्स को inconsistent छोड़ सकते हैं। MVI में, एक reducer एक ही state ऑब्जेक्ट अपडेट करता है, इसलिए “Saving” और “Disabled” में विरोध की संभावना कम होती है।
डबल सबमिट और रेस कंडीशंस से बचने के लिए, हर save को एक पहचान वाले इवेंट की तरह ट्रीट करें। अगर यूज़र दो बार Save टॉप करता है या सेव के दौरान एडिट करता है, आपको नियम चाहिए कि कौन सा रिस्पॉन्स जीतता है। कुछ safeguards दोनों पैटर्न में काम करते हैं: सेव होते समय Save डिसेबल करें (या taps को debounce करें), हर save को एक requestId (या वर्जन) जोड़ें और stale responses को ignore करें, इन-फ्लाइट काम को कैंसल करें जब यूज़र स्क्रीन छोड़ता है, और परिभाषित करें कि सेव के दौरान एडिट का मतलब क्या है (एक और save queue करें, या फॉर्म को फिर से dirty मार्क करें)।
आंशिक सफलता भी आम है: सर्वर कुछ फील्ड स्वीकार कर लेता है और कुछ को रिजेक्ट कर देता है। इसे स्पष्ट रूप से मॉडल करें। प्रति-फील्ड एरर रखें (और आवश्यकता हो तो प्रति-फील्ड सिंक स्टेटस) ताकि आप overall “Saved” दिखा सकें जबकि किसी फील्ड को अभी भी ठीक करने की ज़रूरत हो।
ऐसे एरर स्टेट्स जिनसे यूज़र रिकवर कर सके
फॉर्म स्क्रीन कई तरह से फेल होती हैं, सिर्फ “कुछ ग़लत हुआ” से ज़्यादा। अगर हर फेल्यर एक generic टूस्ट बन जाए, यूज़र फिर से डेटा टाइप करेगा, भरोसा खो देगा, और फ्लो छोड़ देगा। लक्ष्य हमेशा एक ही है: इनपुट सुरक्षित रखें, स्पष्ट फिक्स दिखाएँ, और retry को सामान्य बनाएं।
यह मदद करती है कि एरर को अलग-अलग जगहों में बाँटा जाए। गलत ईमेल फॉर्मेट सर्वर आउटेज जैसा नहीं है।
फील्ड एरर inline और संबंधित इनपुट से जुड़े होने चाहिए। फॉर्म-लेवल एरर सबमिट के पास होने चाहिए और बताना चाहिए कि क्या सबमिशन ब्लॉक कर रहा है। नेटवर्क एरर retry ऑफ़र करें और फॉर्म एडिटेबल रखें। परमिशन या ऑथ एरर यूज़र को re-auth की ओर गाइड करें जबकि ड्राफ्ट बचा रहे।
एक मूल रिकवरी नियम: फेल्यर पर यूज़र इनपुट कभी न मिटाएँ। अगर सेव फेल हो, तो वर्तमान वैल्यू मेमोरी और डिस्क दोनों पर रखें। retry वही payload फिर से भेजे जब तक यूज़र ने कुछ एडिट न किया हो।
जहाँ पैटर्न अलग होते हैं वह यह है कि सर्वर एरर को UI स्टेट में कैसे मैप किया जाए। MVVM में, कई flows या फील्ड्स अपडेट करना आसान है और गलती से inconsistencies बन सकती हैं। MVI में, आप आमतौर पर सर्वर रिस्पॉन्स को एक reducer स्टेप में लागू करते हैं जो fieldErrors और formError दोनों को साथ में अपडेट करता है।
और तय करें कि क्या state है और क्या one-time effect। Inline errors और “submission failed” state में होने चाहिए (ये रोटेशन में भी बने रहेंगे)। स्नैकबार, वाइब्रेशन, या नेविगेशन जैसे वन-ऑफ actions इफेक्ट्स होने चाहिए।
ऑफ़लाइन ड्राफ्ट और इन-प्रोग्रेस फॉर्म्स को रिस्टोर करना
फॉर्म-भारी ऐप ऑफ़लाइन जैसा महसूस कराता है भले ही नेटवर्क ठीक हो। यूज़र ऐप बदलता है, OS आपका प्रोसेस मार देता है, या सिग्नल बीच में कट जाता है। ड्राफ्ट उन्हें फिर से शुरू करने से रोकते हैं।
पहले, परिभाषित करें कि ड्राफ्ट क्या है। सिर्फ “clean” मॉडल सेव करना अक्सर काफी नहीं होता। आप आमतौर पर स्क्रीन को ठीक वैसे ही रिस्टोर करना चाहेंगे जैसे वह दिख रही थी, आधा-टाइप किए फील्ड्स सहित।
क्विट करने लायक चीज़ें ज्यादातर रॉ यूज़र इनपुट होते हैं (जैसे टाइप किए गए स्ट्रिंग्स, चुने गए IDs, अटैचमेंट URIs), साथ में इतना मेटाडेटा कि बाद में सुरक्षित रूप से merge किया जा सके: last-known server snapshot और एक वर्जन मार्कर (updatedAt, ETag, या简单 increment)। वैलिडेशन को restore पर फिर से कम्प्यूट करें।
स्टोरेज विकल्प संवेदनशीलता और साइज़ पर निर्भर करता है। छोटे ड्राफ्ट प्रेफरेंसेज़ में रह सकते हैं, लेकिन मल्टी-स्टेप फॉर्म और अटैचमेंट लोकल डेटाबेस में सुरक्षित रहते हैं। अगर ड्राफ्ट में व्यक्तिगत डेटा है, तो एन्क्रिप्टेड स्टोरेज का उपयोग करें।
सबसे बड़ा आर्किटेक्चरल प्रश्न यह है कि ट्रूथ कहां रहती है। MVVM में, टीमें अक्सर ViewModel से प्रत्येक फील्ड बदलने पर persist करती हैं। MVI में, हर reducer अपडेट के बाद persist करना सरल हो सकता है क्योंकि आप एक coherent state (या derived Draft ऑब्जेक्ट) सेव कर रहे होते हैं।
ऑटोसेव टाइमिंग मायने रखती है। हर कीस्ट्रोक पर सेव करना noisy है; एक छोटा debounce (उदाहरण के लिए 300–800 ms) और स्टेप चेंज पर सेव अच्छा होता है।
जब यूज़र ऑनलाइन वापस आता है, तो merge नियम चाहिए। व्यावहारिक तरीका: अगर सर्वर वर्जन unchanged है, तो ड्राफ्ट लागू करें और सबमिट करें। अगर बदल गया है, तो एक स्पष्ट विकल्प दिखाएँ: मेरा ड्राफ्ट रखें या सर्वर डेटा reload करें।
चरण-दर-चरण: किसी भी पैटर्न से एक भरोसेमंद फॉर्म इम्प्लीमेंट करें
भरोसेमंद फॉर्म साफ़ नियमों से शुरू होते हैं, न कि UI कोड से। हर यूज़र एक्शन को एक प्रेडिक्टेबल स्टेट में बदलना चाहिए, और हर async रिज़ल्ट का एक स्पष्ट landing होना चाहिए।
लिखें कि आपकी स्क्रीन किन-किन actions पर प्रतिक्रिया देनी चाहिए: typing, focus loss, submit, retry, और step navigation। MVVM में ये ViewModel मेथड और स्टेट अपडेट बनते हैं। MVI में ये explicit intents बनते हैं।
फिर छोटे पास में बनाएं:
- पूरे लाइफसाइकल के लिए events परिभाषित करें: edit, blur, submit, save success/failure, retry, restore draft.
- एक state ऑब्जेक्ट डिज़ाइन करें: field values, per-field errors, overall form status, और "has unsaved changes".
- वैलिडेशन जोड़ें: एडिटिंग के दौरान हल्की जाँच, सबमिट पर भारी जाँच।
- optimistic save नियम जोड़ें: क्या तुरंत बदलता है, और क्या rollback ट्रिगर करता है।
- ड्राफ्ट जोड़ें: debounce के साथ autosave, ओपन पर restore, और एक छोटा “draft restored” संकेतक दिखाएँ ताकि यूज़र भरोसा करे।
एरर को अनुभव का हिस्सा समझें। इनपुट रखें, सिर्फ वही हाइलाइट करें जिसे ठीक करने की ज़रूरत है, और एक स्पष्ट अगला कदम दें (edit, retry, या keep draft)।
अगर आप Android UI लिखने से पहले जटिल फॉर्म स्टेट्स प्रोटोटाइप करना चाहते हैं, तो कोई नो-कोड प्लेटफ़ॉर्म जैसे AppMaster (appmaster.io) वर्कफ़्लो को पहले validate करने में उपयोगी हो सकता है। फिर आप वही नियम MVVM या MVI में लागू कर सकते हैं बिना ज़्यादा-surprises के।
उदाहरण परिदृश्य: मल्टी-स्टेप खर्च रिपोर्ट फॉर्म
कल्पना कीजिए एक 4-स्टेप खर्च रिपोर्ट: details (date, category, amount), receipt upload, notes, फिर review और submit। सबमिट के बाद यह एक approval स्टेट दिखाती है जैसे Draft, Submitted, Rejected, Approved। जटिल हिस्से हैं वैलिडेशन, फेल होने वाली सेव, और फोन ऑफ़लाइन होने पर ड्राफ्ट को रखना।
MVVM में, आप आमतौर पर ViewModel में FormUiState रखते हैं (अक्सर एक StateFlow)। हर फील्ड चेंज onAmountChanged() या onReceiptSelected() जैसी ViewModel फ़ंक्शन कॉल करता है। वैलिडेशन चेंज पर, स्टेप नेविगेशन पर, या सबमिट पर चलता है। एक सामान्य संरचना है: रॉ इनपुट्स प्लस फील्ड एरर्स, और derived flags जो Next/Submit को enable करते हैं।
MVI में, वही फ्लो explicit हो जाता है: UI intents भेजता है जैसे AmountChanged, NextClicked, SubmitClicked, और RetrySave। एक reducer नया state लौटाता है। साइड-इफेक्ट्स (receipt upload, API call, snackbar) reducer के बाहर चलते हैं और परिणाम के रूप में इवेंट्स वापस आते हैं।
व्यावहारिक रूप से, MVVM में फंक्शन्स जोड़ना और फ्लो जल्दी अपडेट करना आसान है। MVI में गलती से स्टेट ट्रांज़िशन स्किप करना मुश्किल होता है क्योंकि हर बदलाव reducer के माध्यम से funnel होता है।
सामान्य गलतियाँ और जाल
ज्यादातर फॉर्म बग्स अस्पष्ट नियमों से आते हैं कि ट्रूथ किसके पास है, वैलिडेशन कब चलता है, और असिंक्रोनस रिज़ल्ट आने पर क्या होता है।
सबसे आम गलती है सोर्स ऑफ ट्रूथ का मिलना-जुलना। अगर एक टेक्स्ट फील्ड कभी widget से पढ़ता है, कभी ViewModel स्टेट से, और कभी restored draft से, तो आपको रैंडम resets और "मेरा इनपुट गायब हो गया" रिपोर्ट मिलेंगे। एक canonical state चुनें और सब कुछ उससे derive करें (domain model, cache rows, API payloads)।
एक और आसान जाल है state और events को मिलाना। टूस्ट, नेविगेशन, या “Saved!” बैनर एक-बार का इवेंट है। एक एरर मैसेज जो तब तक दिखना चाहिए जब तक यूज़र एडिट न करे वह state है। इन्हें मिलाने से रोटेशन पर duplicative effects या missing feedback होता है।
दो correctness मुद्दे अक्सर दिखते हैं:
- हर कीस्ट्रोक पर ओवर-वैधेशन, खासकर महँगे चेक्स के लिए। Debounce करें, blur पर validate करें, या सिर्फ touched fields को validate करें।
- आउट-ऑफ-ऑर्डर async परिणामों की अनदेखी। अगर यूज़र दो बार सेव करता है या सेव के बाद एडिट करता है, पुराने responses नए इनपुट को ओवरराइट कर सकते हैं जब तक आप request IDs (या "latest only" लॉजिक) न उपयोग करें।
अंत में, ड्राफ्ट सिर्फ "JSON सेव कर दो" नहीं हैं। वर्जनिंग के बिना, ऐप अपडेट restores को तोड़ सकते हैं। एक सरल schema version और migration कहानी जोड़ें, भले ही बहुत पुराने ड्राफ्ट के लिए रणनीति "ड्रॉप और fresh शुरू करें" हो।
शिप करने से पहले त्वरित चेकलिस्ट
MVVM बनाम MVI पर बहस करने से पहले, यह सुनिश्चित करें कि आपके फॉर्म का एक स्पष्ट सोर्स ऑफ ट्रूथ है। अगर कोई वैल्यू स्क्रीन पर बदल सकती है, तो वह state में होनी चाहिए, न कि view widget में या किसी hidden flag में।
एक व्यावहारिक प्री-शिप चेक:
- State में inputs, field errors, save status (idle/saving/saved/failed), और draft/queue status शामिल हों ताकि UI को अंदाज़ा न लगाना पड़े।
- Validation rules pure और UI के बिना टेस्टेबल हों।
- Optimistic UI का rollback path मौजूद हो यदि सर्वर reject करे।
- Errors कभी यूज़र इनपुट नहीं मिटाएँ।
- Draft restore predictable हो: या तो एक स्पष्ट auto-restore बैनर या एक explicit “Restore draft” क्रिया।
एक और टेस्ट जो असली बग पकड़ता है: सेव के दौरान एयरप्लेन मोड चालू करें, फिर बंद करें, फिर दो बार retry करें। दूसरी retry डुप्लिकेट नहीं बनानी चाहिए। request ID, idempotency key, या लोकल “pending save” मार्कर का उपयोग करें ताकि retries सुरक्षित रहें।
अगर आपके जवाब धुंधले हैं, तो पहले स्टेट मॉडल को कसा करें, फिर वह पैटर्न चुनें जो उन नियमों को लागू करना सबसे आसान बनाता है।
आगे के कदम: रास्ता चुनना और तेज़ी से बनाना
एक प्रश्न से शुरू करें: अगर आपका फॉर्म किसी अजीब आधा-अपडेटेड स्टेट में फँस जाए तो नुकसान कितना बड़ा होगा? अगर नुकसान कम है, तो सरल रखें।
MVVM अच्छा फ़िट है जब स्क्रीन सीधी-सीधी हो, स्टेट ज्यादातर “fields + errors” हो, और आपकी टीम पहले से ViewModel + LiveData/StateFlow के साथ आराम से शिप करती हो।
MVI बेहतर फ़िट है जब आपको सख़्त, प्रेडिक्टेबल स्टेट ट्रांज़िशन चाहिए, बहुत सारे async इवेंट (autosave, retry, sync) हों, या बग्स महंगे हों (payments, compliance, critical workflows)।
जो भी पाथ चुनें, फॉर्म के लिए सबसे अधिक उपयोगी टेस्ट आम तौर पर UI को नहीं छूते: वैलिडेशन एज केस, स्टेट ट्रांज़िशन (edit, submit, success, failure, retry), optimistic save rollback, और ड्राफ्ट restore + conflict व्यवहार।
अगर आपको बैकएंड, एडमिन स्क्रीन, और APIs भी चाहिए मोबाइल के साथ, तो AppMaster (appmaster.io) एक ही मॉडल से production-ready backend, web, और native mobile apps जनरेट कर सकता है, जिससे वर्कफ़्लो नियम और वैलिडेशन सतहों के बीच consistente रहते हैं।
सामान्य प्रश्न
Pick MVVM when your form flow is mostly linear and your team already has solid conventions for StateFlow/LiveData, one-off events, and cancellation. Pick MVI when you expect lots of overlapping async work (autosave, retries, uploads) and you want stricter rules so state changes can’t “sneak in” from multiple places.
Start with a single screen state object (for example, FormState) that contains raw field values, field-level errors, a form-level error, and clear statuses like Saving or Failed. Keep derived flags like isValid and canSubmit computed in one place so the UI only renders, not re-decides logic.
Run light, cheap checks while the user edits (required, range, basic format), and run strict checks on submit. Keep validation code out of the UI so it’s testable, and store errors in state so they survive rotation and process death restores.
Treat async validation as “latest input wins.” Store the value you validated (or a request/version id) and ignore results that don’t match the current state. This prevents stale responses from overwriting newer typing, which is a common source of “random” error messages.
Update the UI immediately to reflect the action (for example, show Saving… and keep the input visible), but always keep a rollback path if the server rejects the save. Use a request id/version, disable or debounce the Save button, and define what edits during save mean (lock fields, queue another save, or mark dirty again).
Never clear user input on failure. Put field-specific problems inline on the relevant fields, keep form-level blockers near the submit action, and make network failures recoverable with a retry that resends the same payload unless the user changes something.
Keep one-off effects out of your persistent state. In MVVM, send them through a separate stream (like a SharedFlow), and in MVI, model them as Effects that the UI consumes once. This avoids duplicate snackbars or repeated navigation after rotation or re-subscription.
Persist mostly raw user input (as typed), plus minimal metadata to restore and merge safely later, like a last-known server version marker. Recompute validation on restore instead of persisting it, and add a simple schema version so you can handle app updates without breaking restores.
Use a short debounce (around a few hundred milliseconds) plus saves on step changes or when the user backgrounds the app. Saving on every keystroke is noisy and can create extra contention, while saving only on exit risks losing work during process death or interruptions.
Keep a version marker (like updatedAt, an ETag, or a local increment) for both the server snapshot and the draft. If the server version hasn’t changed, apply the draft and submit; if it has, show a clear choice to keep the draft or reload server data, rather than silently overwriting either side.


