Kotlin MVI বনাম MVVM ফর্ম-ভারী Android অ্যাপের জন্য: UI স্টেট
Kotlin MVI বনাম MVVM: ফর্ম-ভিত্তিক Android অ্যাপে ভ্যালিডেশন, অপ্টিমিস্টিক UI, এরর স্টেট, ও অফলাইন ড্রাফ্ট মডেল করার ব্যবহারিক উপায়।

কেন ফর্ম-ভিত্তিক Android অ্যাপ দ্রুত জটিল হয়ে যায়
ফর্ম-ভিত্তিক অ্যাপগুলো ধীর বা ভঙ্গুর মনে হতে পারে কারণ ব্যবহারকারীরা বারবার এমন ছোট সিদ্ধান্তের জন্য অপেক্ষা করে যা আপনার কোডকে নিতে হয়: এই ফিল্ডটি বৈধ কি না, সেভ কাজ করেছে কি না, কি সময়ে একটি এরর দেখাবো, এবং নেটওয়ার্ক চলে গেলে কি হবে।
ফর্মগুলো প্রথমেই স্টেট-বাগগুলো প্রকাশ করে কারণ এগুলো একযোগে কয়েক ধরনের স্টেট মিশায়: UI স্টেট (কি দেখা যাচ্ছে), ইনপুট স্টেট (ব্যবহারকারী যা টাইপ করেছে), সার্ভার স্টেট (কি সেভ হয়েছে), এবং অস্থায়ী স্টেট (কি প্রক্রিয়াধীন)। যখন এসব একসাথে সিঙ্ক থেকে বিচ্ছিন্ন হয়, অ্যাপটি “যেমন-বসে” মনে হয়: বোতামগুলি ভুল সময়ে নিষ্ক্রিয় হয়, পুরোনো এরর দেখা যায়, বা স্ক্রিন রোটেশনের পরে রিসেট হয়।
বেশিরভাগ সমস্যা চারটি এলাকায় ঘনিষ্ঠভাবে গুচ্ছবদ্ধ: ভ্যালিডেশন (বিশেষত ক্রস-ফিল্ড নিয়ম), অপ্টিমিস্টিক UI (কাজ চলতেই দ্রুত ফিডব্যাক), এরর হ্যান্ডলিং (পরিষ্কার, পুনরুদ্ধারযোগ্য ফল), এবং অফলাইন ড্রাফ্ট (অসম্পূর্ণ কাজ হারাবেন না)।
ভাল ফর্ম UX কয়েকটি সরল নিয়ম অনুসরণ করে:
- ভ্যালিডেশন হওয়া উচিত সহায়ক এবং ফিল্ডের কাছে। টাইপিং ব্লক করবেন না। যখন গুরুত্ব থাকে তখনই কড়া হোন, সাধারণত সাবমিটে।
- অপ্টিমিস্টিক UI ব্যবহারকারীর ক্রিয়াকে ত্বরিতভাবে প্রতিফলিত করা উচিত, কিন্তু সার্ভার প্রত্যাখ্যান করলে পরিষ্কার রোলব্যাক থাকা উচিত।
- এররগুলো সুনির্দিষ্ট, কার্যকর এবং কখনও ব্যবহারকারীর ইনপুট মুছবে না।
- ড্রাফ্টগুলো রিস্টার্ট, বাধ্যচ্ছেদ, ও খারাপ কানেকশনের সময় টিকে থাকা উচিত।
এই কারণেই ফর্ম নিয়ে আর্কিটেকচার আলোচনা তীব্র হয়। আপনি যে প্যাটার্ন বাছেন তা নির্ধারণ করে যে চাপের মধ্যে এসব স্টেট কতটা প্রত্যাশাযোগ্য মনে হবে।
দ্রুত রিফ্রেশার: MVVM এবং MVI সরলভাবে
MVVM এবং MVI-এর মধ্যে বাস্তব পার্থক্য হল কিভাবে change একটি স্ক্রীনের ভিতর দিয়ে প্রবাহিত হয়।
MVVM (Model View ViewModel) সাধারণত এইরকম দেখায়: ViewModel স্ক্রীনের ডাটা রাখে, UI-তে তা প্রকাশ করে (প্রায়ই StateFlow বা LiveData দিয়ে), এবং save, validate, বা load মত মেথড সরবরাহ করে। UI ব্যবহারকারীর ইন্টারঅ্যাকশনের সময় ViewModel ফাংশন কল করে।
MVI (Model View Intent) সাধারণত এইরকম: UI ইভেন্ট (intent) পাঠায়, একটি reducer সেগুলো প্রক্রিয়া করে, এবং স্ক্রীন একটিই state অবজেক্ট থেকে রেন্ডার হয় যা বর্তমানে UI-কে যা দরকার সব কিছু উপস্থাপন করে। সাইড-এফেক্ট (নেটওয়ার্ক, ডাটাবেস) নিয়ন্ত্রিতভাবে ট্রিগার হয় এবং ফলাফল ইভেন্ট হিসেবে রিপোর্ট করে।
মনোভাব স্মরণ রাখার সহজ উপায়:
- MVVM জিজ্ঞেস করে, “ViewModel কি ডাটা প্রকাশ করবে, এবং কি পদ্ধতি থাকবে?”
- MVI জিজ্ঞেস করে, “কোন কোন ইভেন্ট ঘটতে পারে, এবং সেগুলো কিভাবে একটি state-কে রূপান্তর করে?”
সরল স্ক্রিনের জন্য উভয় প্যাটার্নই ভালভাবে কাজ করে। একবার আপনি ক্রস-ফিল্ড ভ্যালিডেশন, অটোসেভ, রিট্রাই, ও অফলাইন ড্রাফ্ট যুক্ত করলে, আপনাকে কঠোর নিয়মের প্রয়োজন হয় যে কে কখন state পরিবর্তন করতে পারে। MVI ডিফল্টভাবে সেই নিয়মগুলো আরোপ করে। MVVM এখনও ভালো কাজ করতে পারে, তবে সেখানে শৃঙ্খলা দরকার: সঙ্গত update পথ এবং এক-বারের UI ইভেন্ট (toasts, navigation) এর সাবধানে হ্যান্ডলিং।
কিভাবে ফর্ম স্টেট মডেল করবেন যাতে বিস্ময় না হয়
সর্বোত্তম দ্রুতভাবে নিয়ন্ত্রণ হারানোর পথ হল ফর্ম ডেটাকে অনেক জায়গায় ছড়িয়ে রাখা: view bindings, একাধিক flows, এবং “আরেকটি” boolean। ফর্ম-ভিত্তিক স্ক্রীনগুলো পূর্বাভাসযোগ্য থাকে যখন একটি একক source of truth থাকে।
একটি বাস্তবসম্মত FormState আকৃতি
কাঁচা ইনপুট এবং কয়েকটি নির্ভরযোগ্য ডেরাইভড ফ্ল্যাগ ধরা একটি একক FormState লক্ষ্য করুন। এটা একটু বড় মনে হলেও শুকরিয়া রেখে নিরপেক্ষ ও সম্পূর্ণ রাখুন।
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 অবশ্যই > 0"). isDirty ও isValid মত ডেরাইভড ফ্ল্যাগগুলো একটি জায়গায় গণনা করা উচিত, UI-তে পুনরায় বাস্তবায়ন করা উচিত নয়।
একটি পরিষ্কার মানসিক মডেল হল: fields (ব্যবহারকারী কি টাইপ করেছে), validation (কী ভুল), status (অ্যাপ কি করছে), dirtiness (শেষ সেভ থেকে কি বদলেছে), এবং drafts (অফলাইন কপি আছে কি না)।
এক-বারের ইফেক্ট কোথায় থাকা উচিত
ফর্মগুলো অনেক সময় এক-বারের ইভেন্ট ট্রিগার করে: snackbars, navigation, “saved” ব্যানার। এগুলো FormState-এর ভিতরে রাখবেন না, নয়তো রোটেশনের পরে বা UI পুনরায় সাবস্ক্রাইব করলে আবার দেখাবে।
MVVM-এ এগুলো আলাদা চ্যানেল (উদাহরণ: একটি SharedFlow) দিয়ে ইমিট করুন। MVI-তে এগুলো Effects (বা Events) হিসেবে মডেল করুন যেগুলো UI একবারে গ্রহণ করে। এই পৃথকীকরণ “ভূত” এরর ও ডুপ্লিকেট সাফল্য বার্তা রোধ করে।
MVVM বনাম MVI-তে ভ্যালিডেশন ফ্লো
ভ্যালিডেশনই সেই জায়গা যেখানে ফর্ম স্ক্রিনগুলো সরু মনে হয়। মূল পছন্দ হল নিয়ম কোথায় থাকবে এবং ফলাফলগুলো UI-তে কিভাবে ফিরে যাবে।
সরল, synchronous নিয়ম (required fields, min length, number ranges) ViewModel বা ডোমেইন লেয়ারে চালানো উচিত, UI-তে নয়। এতে নিয়মগুলো টেস্টযোগ্য ও সঙ্গত থাকে।
অ্যাসিঙ্ক নিয়ম (যেমন "এই ইমেলটি ইতিমধ্যেই নেওয়া?") আরও জটিল। আপনাকে লোডিং, stale ফলাফল, এবং "ব্যবহারকারী আবার টাইপ করল" কেস হ্যান্ডল করতে হবে।
MVVM-এ, ভ্যালিডেশন প্রায়ই state ও হেল্পার মেথডের মিশ্রণ হয়: UI পরিবর্তন (টেক্সট আপডেট, ফোকাস পরিবর্তন, সাবমিট ক্লিক) ViewModel-কে পাঠায়; ViewModel একটি StateFlow/LiveData আপডেট করে এবং প্রতিটি ফিল্ডের এরর ও একটি ডেরাইভড canSubmit এক্সপোজ করে। অ্যাসিঙ্ক চেকগুলো সাধারণত একটি জব শুরু করে, তারপর যখন শেষ হয় তখন একটি লোডিং ফ্ল্যাগ ও একটি এরর আপডেট করে।
MVI-তে, ভ্যালিডেশন আরও স্পষ্ট হয়ে ওঠে। একটি বাস্তবসম্মত দায়িত্ব বিভাগ হতে পারে:
- reducer synchronous ভ্যালিডেশন চালায় এবং অবিলম্বে
fieldErrorsআপডেট করে। - একটি effect asynchronous ভ্যালিডেশন চালায় এবং একটি result intent ডিসপ্যাচ করে।
- reducer সেই result প্রয়োগ করে শুধুমাত্র যদি তা এখনও সর্বশেষ ইনপুটের সাথে মেলে।
শেষ ধাপটা গুরুত্বপূর্ণ। যদি ব্যবহারকারী নতুন ইমেল টাইপ করে যখন "unique email" চেক চলছে, পুরোনো ফলাফল বর্তমান ইনপুটকে ওভাররাইট করা উচিত নয়। MVI প্রায়ই এটিকে কোড করার জন্য সহজ করে তোলে কারণ আপনি state-এ শেষ-চেক করা মান রাখতে পারেন এবং stale প্রতিক্রিয়াগুলো উপেক্ষা করতে পারেন।
অপ্টিমিস্টিক UI এবং অ্যাসিঙ্ক সেভ
অপ্টিমিস্টিক UI মানে সেভের আগে স্ক্রীনটি এমন আচরণ করে যেন সেভ কাজ করেছে। ফর্মে এটাও হতে পারে: Save বোতাম "Saving..."-এ পালটায়, একটি ছোট "Saved" ইন্ডিকেটর দেখায় যখন কাজ শেষ হয়, এবং ইনপুট ব্যবহারযোগ্য থাকে (বা ইচ্ছা করে লক করা হয়) অনুরোধ চলাকালীন।
MVVM-এ, এটি সাধারণত isSaving, lastSavedAt, এবং saveError মত ফ্ল্যাগ টগল করে বাস্তবায়িত হয়। ঝুঁকি হল drift: ওভারল্যাপিং সেভগুলো এই ফ্ল্যাগগুলিকে অসংগত করে ফেলতে পারে। MVI-তে reducer একটাই state অবজেক্ট আপডেট করে, তাই "Saving" ও "Disabled" মিলামিশা হওয়ার সম্ভাবনা কম।
ডাবল সাবমিট ও রেস কন্ডিশন এড়াতে, প্রতিটি সেভকে শনাক্তকৃত ইভেন্ট হিসেবে বিবেচনা করুন। যদি ব্যবহারকারী Save দুবার ট্যাপ করে বা সেভ চলাকালীন এডিট করে, কোন রেসপন্স জিতবে তা বলার নিয়ম দরকার। কয়েকটি সেফগার্ড উভয় প্যাটার্নেই কাজে দেয়: সেভ চলাকালীন Save ডিসেবল করা (বা ট্যাপ ডিবাউন্স করা), প্রতিটি সেভে একটি requestId (বা ভার্সন) লাগানো এবং stale রেসপন্স উপেক্ষা করা, ইন-ফ্লাইট কাজকে ব্যবহারকারী ছেড়ে গেলে ক্যানসেল করা, এবং সংজ্ঞায়িত করা যে সেভ চলাকালীন এডিট মানে কি (আরেকটি সেভ কিউ করা, বা ফর্ম আবার dirty চিহ্নিত করা)।
পার্শিয়াল সাকসেসও সাধারণ: সার্ভার কিছু ফিল্ড গ্রহণ করে কিন্তু কিছু প্রত্যাখ্যান করে। এটাকে স্পষ্টভাবে মডেল করুন। প্রতিটি ফিল্ডের এরর (এবং প্রয়োজনে প্রতিটি ফিল্ডের সিঙ্ক স্ট্যাটাস) রাখুন যাতে আপনি সামগ্রিকভাবে "Saved" দেখাতে পারেন ঠিকই, কিন্তু যে ফিল্ডটি সমস্যা তাতে হাইলাইট দেখাতে পারেন।
ব্যবহারকারী পুনরুদ্ধার করতে পারার মতো এরর স্টেট
ফর্ম স্ক্রীনগুলো "কিছু ভুল হয়েছে" ছাড়াও আরও অনেকভাবে ব্যর্থ হয়। যদি প্রতিটি ব্যর্থতাই একটি সাধারণ টোস্ট হয়ে যায়, ব্যবহারকারীরা ডাটা পুনরায় টাইপ করে, বিশ্বাস হারায়, এবং ফ্লো ছেড়ে দিতে পারে। লক্ষ্য সবসময় একই: ইনপুট নিরাপদ রাখুন, পরিষ্কার সমাধান দেখান, এবং রিট্রাই স্বাভাবিকভাবে মনে করান।
এররগুলোকে কোথায় সম্পর্কিত তা আলাদা করলে সাহায্য হয়। ভুল ইমেল ফরম্যাটটি সার্ভার আউটেজের সমান নয়।
ফিল্ড এররগুলো ইনলাইন এবং একটি ইনপুটের সাথে বাঁধা থাকা উচিত। ফর্ম-লেভেল এররগুলো সাবমিট অ্যাকশনের কাছে থাকা উচিত এবং কি জিনিস সাবমিশন ব্লক করছে তা ব্যাখ্যা করা উচিত। নেটওয়ার্ক এররগুলো রিট্রাই অফার করা উচিত এবং ফর্ম এডিটেবল রেখে। পারমিশন বা অথ এররগুলো ব্যবহারকারীকে পুনরায় অথেন্টিকেট করার নির্দেশ দেবে কিন্তু ড্রাফ্ট সংরক্ষণ করবে।
একটি মূল পুনরুদ্ধার নিয়ম: ব্যর্থতার ওপর ব্যবহারকারীর ইনপুট কখনই মুছবেন না। সেভ ব্যর্থ হলে, বর্তমান মানগুলো মেমরি ও ডিস্কে রাখুন। রিট্রাই একই পে-লোড পুনরায় পাঠাবে যতক্ষণ ব্যবহারকারী কিছু না বদলে।
প্যাটার্নগুলো ভিন্ন হয় কিভাবে সার্ভার এররগুলো UI স্টেটে ম্যাপ হয়। MVVM-এ, একাধিক flow বা ফিল্ড আপডেট করে সহজেই অসংগতি তৈরি করা যায়। MVI-তে সাধারণত সার্ভার রেসপন্স একবারের reducer স্টেপে প্রয়োগ করা হয় যা fieldErrors ও formError একসাথে আপডেট করে।
এছাড়াও সিদ্ধান্ত নিন কি state আর কি এক-বারের effect। ইনলাইন এরর ও "submission failed" state-এ থাকা উচিত (সেগুলো রোটেশান সহ্য করবে)। এক-বারের অ্যাকশন যেমন snackbar, vibration, বা navigation effect হওয়া উচিত।
অফলাইন ড্রাফ্ট ও ইন-প্রগ্রেস ফর্ম রিস্টোর
ফর্ম-ভিত্তিক একটি অ্যাপ "অফলাইন" মনে হতে পারে এমনকি নেটওয়ার্ক ঠিক থাকলেও। ব্যবহারকারীরা অ্যাপ সুইচ করে, OS আপনার প্রসেস কিল করে, বা সিগন্যাল হারিয়ে ফেলে মাঝপথে। ড্রাফ্টগুলো তাদের পুনরায় শুরু করা থেকে রক্ষা করে।
প্রথমে নির্ধারণ করুন ড্রাফ্ট মানে কি। শুধু “ক্লিন” মডেল সেভ করা প্রায়ই যথেষ্ট নয়। সাধারণত আপনি চাইবেন স্ক্রীনটা ঠিক তেমনি রিস্টোর করতে, অর্ধেক টাইপ করা ফিল্ডগুলোসহ।
যা সেভ করা মূল্যবান তা প্রধানত কাঁচা ব্যবহারকারী ইনপুট (টাইপ করা স্ট্রিং, নির্বাচিত IDs, সংযুক্তি URI), এবং পরে নিরাপদভাবে মার্জ করার জন্য পর্যাপ্ত মেটাডেটা: একটি last-known server snapshot এবং একটি ভার্সন মার্কার (updatedAt, ETag, বা একটি ইনক্রিমেন্ট)। ভ্যালিডেশন রিস্টোরে পুনরায় গণনা করা যেতে পারে।
স্টোরেজ পছন্দ সংবেদনশীলতা ও আকারের উপর নির্ভর করে। ছোট ড্রাফ্ট প্রেফারেন্সে থাকতে পারে, কিন্তু মাল্টি-স্টেপ ফর্ম ও অ্যাটাচমেন্ট লোকাল ডাটাবেসে রাখা নিরাপদ। ড্রাফ্টে ব্যক্তিগত তথ্য থাকলে এনক্রিপ্টেড স্টোরেজ ব্যবহার করুন।
সবচেয়ে বড় আর্কিটেকচার প্রশ্ন হল সত্যির সুত্র কোথায় থাকে। MVVM-এ, টিমগুলো প্রায়ই ViewModel থেকে প্রতিটি ফিল্ড পরিবর্তনে persist করে। MVI-তে প্রতিটি reducer আপডেটের পরে persist করা সহজ হতে পারে কারণ আপনি একত্রীভূত state (বা একটি ডেরাইভড Draft অবজেক্ট) সেভ করছেন।
অটোসেভ টাইমিং গুরুত্বপূর্ণ। প্রতিটি কীস্ট্রোক-এ সেভ করা শব্দতরকি; একটি সংক্ষিপ্ত ডিবাউন্স (যেমন 300–800 ms) ও ধাপ পরিবর্তনের সময় সেভ করা ভালো কাজ করে।
যখন ব্যবহারকারী অনলাইনে ফিরে আসে, আপনাকে মার্জ নিয়ম দরকার। একটি ব্যবহারিক পদ্ধতি হল: যদি সার্ভার ভার্সন অপরিবর্তিত থাকে, ড্রাফ্ট প্রয়োগ করে সাবমিট করুন। যদি বদলায়, একটি স্পষ্ট সিদ্ধান্ত দেখান: আমার ড্রাফ্ট রাখুন নাকি সার্ভার ডেটা রিলোড করুন।
ধাপে ধাপে: উভয় প্যাটার্নে একটি নির্ভরযোগ্য ফর্ম ইমপ্লিমেন্ট করা
নির্ভরযোগ্য ফর্মগুলো UI কোড নয়, পরিষ্কার নিয়ম দিয়ে শুরু হয়। প্রতিটি ব্যবহারকারীর ক্রিয়াই একটি প্রত্যাশাযোগ্য স্টেটের দিকে নিয়ে যাবে, এবং প্রতিটি অ্যাসিঙ্ক ফলাফলের একটি স্পষ্ট অবতরণ স্থল থাকা উচিত।
আপনার স্ক্রীনকে প্রতিক্রিয়া দেওয়ার জন্য যে অ্যাকশনগুলো দরকার সেগুলো লিখে ফেলুন: টাইপিং, ফোকাস লস, সাবমিট, রিট্রাই, ও ধাপ নেভিগেশন। MVVM-এ এইগুলো ViewModel মেথড ও স্টেট আপডেটে পরিণত হবে। MVI-তে এগুলো স্পষ্ট intents হবে।
তারপর ছোট পাসে গড়ে তুলুন:
- ফুল লাইফসাইকেলের জন্য ইভেন্টগুলি ডিফাইন করুন: edit, blur, submit, save success/failure, retry, restore draft.
- একটি state অবজেক্ট ডিজাইন করুন: ফিল্ড ভ্যালু, ফিল্ড-ভিত্তিক এরর, সামগ্রিক ফর্ম স্ট্যাটাস, এবং "has unsaved changes"।
- ভ্যালিডেশন যোগ করুন: এডিটিংয়ের সময় হালকা চেক, সাবমিটে ভারী চেক।
- অপ্টিমিস্টিক সেভ নিয়ম যোগ করুন: কি অবিলম্বে বদলাবে, আর কি রোলব্যাক ট্রিগার করবে।
- ড্রাফ্ট যোগ করুন: একটি ডিবাউন্স সহ অটোসেভ, ওপেন করার সময় রিস্টোর, এবং একটি ছোট "draft restored" ইন্ডিকেটর দেখান যাতে ব্যবহারকারী যা দেখে তাতে বিশ্বাস রাখে।
এররগুলোকে অভিজ্ঞতার একটি অংশ হিসেবে বিবেচনা করুন। ইনপুট রাখুন, শুধুই যেটা ঠিক করা দরকার সেটাই হাইলাইট করুন, এবং একটি পরিষ্কার পরবর্তী ক্রিয়া অফার করুন (edit, retry, বা keep draft)।
জটিল ফর্ম স্টেটগুলো Android UI লেখার আগে প্রোটোটাইপ করতে চাইলে, AppMaster একটি নো-কোড প্ল্যাটফর্ম হিসাবে কার্যকর হতে পারে ওয়ার্কফ্লো প্রথমে যাচাই করার জন্য। তারপর আপনি একই নিয়মগুলো MVVM বা MVI-তে অপেক্ষাকৃত কম বিস্ময় নিয়ে বাস্তবায়ন করতে পারবেন।
উদাহরণ পরিস্থিতি: মাল্টি-স্টেপ খরচ রিপোর্ট ফর্ম
ধরা যাক 4-ধাপের একটি খরচ রিপোর্ট: বিবরণ (তারিখ, শ্রেণী, পরিমাণ), রসিদ আপলোড, নোটস, এরপর রিভিউ ও সাবমিট। সাবমিটের পরে এটা একটি অনুমোদন স্ট্যাটাস দেখায় যেমন Draft, Submitted, Rejected, Approved। জটিল অংশগুলো হল ভ্যালিডেশন, সেভ ব্যর্থ হওয়া, এবং ফোন অফলাইনে গেলে ড্রাফ্ট রাখা।
MVVM-এ, সাধারণত আপনি ViewModel-এ একটি FormUiState (প্রায়ই একটি StateFlow) রাখেন। প্রতিটি ফিল্ড পরিবর্তন একটি ViewModel ফাংশন কল করে, যেমন onAmountChanged() বা onReceiptSelected()। ভ্যালিডেশন পরিবর্তনে, ধাপ পরিবর্তনে, বা সাবমিটে চলে। একটি সাধারণ কাঠামো কাঁচা ইনপুট ও ফিল্ড এররস সহ থাকে, ডেরাইভড ফ্ল্যাগগুলো Next/Submit সক্ষম করে।
MVI-তে, একই ফ্লো স্পষ্ট হয়: UI intents পাঠায় যেমন AmountChanged, NextClicked, SubmitClicked, ও RetrySave। একটি reducer নতুন state ফেরত দেয়। সাইড-এফেক্টগুলো (রসিদ আপলোড, API কল, snackbar দেখানো) reducer-এর বাইরে চলে এবং ফলাফল ইভেন্ট হিসেবে ফিরে আসে।
অনুশীলনে, MVVM দ্রুত ফাংশন যোগ করে একটি flow আপডেট করা সহজ করে। MVI ভুল করে একটি state transition মিস করা কঠিন করে দেয় কারণ প্রতিটি পরিবর্তন reducer-এ ফানেল করা হয়।
সাধারণ ভুল ও ফাঁদ
বেশিরভাগ ফর্ম বাগ আসে সত্যির মালিক কে তা অস্পষ্ট হওয়া, কখন ভ্যালিডেশন চলে, এবং অ্যাসিঙ্ক ফলাফল লেট এলে কি হবে তা নিয়ে অনিশ্চয়তা থেকে।
সবচেয়ে সাধারণ ভুল হল truth-এর উৎস মিশানো। যদি একটি টেক্সট ফিল্ড কখনও widget থেকে পড়ে, কখনও ViewModel state থেকে, এবং কখনও রিস্টোর করা ড্রাফ্ট থেকে—তাহলে র্যান্ডম রিসেট ও “আমার ইনপুট হারালো” রিপোর্ট পাবেন। একটি ক্যাননিক্যাল state বাছুন এবং স্ক্রীনের সবকিছু তা থেকেই derive করুন (ডোমেইন মডেল, ক্যাশ রো, API পে-লোড)।
আরেকটি সহজ ফাঁদ হল state এবং events আলাদা না করা। একটি টোস্ট, নেভিগেশন, বা “Saved!” ব্যানার এক-বারের ইভেন্ট। একটি এরর মেসেজ যা ব্যবহারকারী এডিট না করা পর্যন্ত দৃশ্যমান থাকা উচিত তা state। এরা মিশালে রোটেশনে ডুপ্লিকেট ইফেক্ট বা মিসড ফিডব্যাক হবে।
দুটি correctness সমস্যা প্রায়ই দেখা যায়:
- প্রতিটি কীস্ট্রোক-এ অতিরিক্ত ভ্যালিডেশন চালানো, বিশেষ করে ব্যয়বহুল চেকগুলোর জন্য। ডিবাউন্স করুন, blur-এ ভ্যালিডেট করুন, বা শুধুমাত্র touched ফিল্ডগুলো ভ্যালিডেট করুন।
- আউট-অফ-অর্ডার অ্যাসিঙ্ক ফলাফল ইগনোর করা। যদি ব্যবহারকারী দুবার সেভ করে বা সেভের পরে এডিট করে, পুরোনো রেসপন্স নতুন ইনপুট ওভাররাইট করতে পারে যদি আপনি request IDs (বা "latest only" লজিক) ব্যবহার না করেন।
সবশেষে, ড্রাফ্টগুলো ধারাবাহিকভাবে "শুধু JSON সেভ" নয়। ভার্সনিং ছাড়া, অ্যাপ আপডেটগুলো রিস্টোর ভাঙতে পারে। একটি সহজ স্কিমা ভার্সন এবং একটি মাইগ্রেশন স্টোরি যোগ করুন, এমনকি খুব পুরোনো ড্রাফ্টগুলোর জন্য "ড্রপ করে নতুন শুরু" কৌশলও রাখুন।
শিপ করার আগে দ্রুত চেকলিস্ট
MVVM বনাম MVI নিয়ে ঝগড়া করার আগে নিশ্চিত করুন আপনার ফর্মের একটি স্পষ্ট source of truth আছে। যদি একটি মান স্ক্রীনে পরিবর্তিত হতে পারে, তা state-এ থাকা উচিত, view widget বা লুকানো ফ্ল্যাগে নয়।
একটি ব্যবহারিক প্রি-শিপ চেক:
- State-এ ইনপুট, ফিল্ড এরর, সেভ স্ট্যাটাস (idle/saving/saved/failed), এবং ড্রাফ্ট/কিউ স্ট্যাটাস আছে যেন UI কখনো অনুমান করতে না হয়।
- ভ্যালিডেশন নিয়মগুলো পিউর ও UI ছাড়া টেস্টযোগ্য।
- অপ্টিমিস্টিক UI-এর রোলব্যাক পথ আছে সার্ভার প্রত্যাখ্যানের জন্য।
- এররগুলো ব্যবহারকারীর ইনপুট মুছে ফেলবে না।
- ড্রাফ্ট রিস্টোর প্রত্যাশাযোগ্য: স্পষ্ট অটো-রিস্টোর ব্যানার বা একটি সুনির্দিষ্ট "Restore draft" অ্যাকশন।
আরেকটি টেস্ট যা প্রকৃত বাগ ধরা দেয়: সেভের মাঝখানে airplane mode চালু করুন, তারপর চালু করুন, তারপর দুবার রিট্রাই করুন। দ্বিতীয় রিট্রাই ডুপ্লিকেট তৈরি করা উচিত নয়। request ID, idempotency key, বা লোকাল "pending save" মার্কার ব্যবহার করে রিট্রাই সেফ করুন।
আপনার জবাবগুলো যদি ফাজি হয়, আগে state মডেল টাইটেন করুন, তারপর সেই নিয়মগুলো প্রয়োগ করা সহজ করে এমন প্যাটার্ন বাছুন।
পরবর্তী ধাপ: একটি পথ বাছাই করা এবং দ্রুত নির্মাণ করা
একটি প্রশ্ন দিয়ে শুরু করুন: যদি আপনার ফর্ম অদ্ভুতভাবে অর্ধ-আপডেটেড অবস্থায় পড়ে যায়, তার ব্যয় কত বড়? যদি ব্যয় কম হয়, সরল রাখুন।
MVVM উপযুক্ত যখন স্ক্রীন সরল, state প্রাথমিকত "fields + errors", এবং আপনার দল ইতিমধ্যেই ViewModel + LiveData/StateFlow দিয়ে আত্মবিশ্বাসী কাস্টম রান করছে।
MVI উপযুক্ত যখন আপনাকে কঠোর, প্রত্যাশাযোগ্য state transition দরকার, প্রচুর অ্যাসিঙ্ক ইভেন্ট (অটোসেভ, রিট্রাই, সিঙ্ক) আছে, অথবা বাগগুলো ব্যয়বহুল (পেমেন্ট, কমপ্লায়েন্স, ক্রিটিক্যাল ওয়ার্কফ্লো)।
যে পথে যান, ফর্মের জন্য উচ্চ-রিটার্ন টেস্টগুলো সাধারণত UI ছাড়া। ভ্যালিডেশন এজ কেস, স্টেট ট্রানজিশন (edit, submit, success, failure, retry), অপ্টিমিস্টিক সেভ রোলব্যাক, এবং ড্রাফ্ট রিস্টোর + কনফ্লিক্ট আচরণ টেস্ট করুন।
যদি আপনার ব্যাকএন্ড, অ্যাডমিন স্ক্রিন, এবং APIs মোবাইল অ্যাপের সাথে থাকা দরকার, AppMaster (appmaster.io) একটি অপশন হিসেবে রয়েছে যা এক মডেল থেকে প্রোডাকশন-রেডি ব্যাকএন্ড, ওয়েব, ও নেটিভ মোবাইল অ্যাপ জেনারেট করতে পারে—এটি ভ্যালিডেশন ও ওয়ার্কফ্লো নিয়মগুলো সারফেস জুড়ে সঙ্গত রাখতে সাহায্য করে।
প্রশ্নোত্তর
পছন্দ করুন MVVM যখন আপনার ফর্ম ফ্লো বেশ সরল ও লিনিয়ার এবং আপনার দল আগে থেকেই StateFlow/LiveData, এক-বারের ইভেন্ট ও ক্যান্সেলেশনের কনভেনশন নিয়ে আত্মবিশ্বাসী। পছন্দ করুন MVI যখন আপনি অনেক ওভারল্যাপিং অ্যাসিঙ্ক কাজ (অটোসেভ, রিট্রাই, আপলোড) আশা করছেন এবং কঠোর নিয়ম চান যাতে স্টেট পরিবর্তন একাধিক স্থানে থেকে “চুপিসারে” না ঢুকে যায়।
একটি একক স্ক্রিন স্টেট অবজেক্ট দিয়ে শুরু করুন (উদাহরণ: FormState) যেখানে র ক-ভ্যালু, ফিল্ড-লেভেল এরর, একটি ফর্ম-লেভেল এরর এবং স্পষ্ট স্টেটাস যেমন Saving বা Failed থাকবে। isValid ও canSubmit মতো ডেরাইভড ফ্ল্যাগগুলো একটি জায়গায় গণনা রাখুন যাতে UI কেবল রেন্ডার করে, লজিক পুনরায় সিদ্ধান্ত না করে।
এডিটিংয়ের সময় হালকা, সস্তা চেকগুলো চালান (required, range, বেসিক ফরম্যাট), এবং সাবমিটের সময় কড়া চেক চালান। ভ্যালিডেশন কোড UI-র বাইরে রাখুন যাতে এটা টেস্টযোগ্য হয় এবং এররগুলো state-এ সঞ্চিত রাখুন যেন তা রোটেশান বা প্রসেস ডেথ থেকে প্রাণে যায়।
অ্যাসিঙ্ক ভ্যালিডেশনকে “সর্বশেষ ইনপুট জয়ী” হিসেবে আচরণ করুন। আপনি যে ভ্যালিডেট করেছিলে সেই মান (বা একটি অনুরোধ/ভার্সন আইডি) state-এ রাখুন এবং সেই মান মিল না হলে ফলাফল উপেক্ষা করুন। এর ফলে পুরনো প্রতিক্রিয়া নতুন টাইপিং-এ ওভাররাইট করবে না—এটি সাধারণত “বেমানান” এরর মেসেজের উৎস।
UI-টিকে তত্ক্ষণাত পরিবর্তন দেখান (উদাহরণ: Saving… দেখান এবং ইনপুট দৃশ্যমান রাখুন), কিন্তু সার্ভার প্রত্যাখ্যানের ক্ষেত্রে সর্বদা একটি রোলব্যাক পথ রাখুন। একটি অনুরোধ id/ভার্সন ব্যবহার করুন, Save বোতাম ডিসেবল বা ডিবাউন্স করুন, এবং সংজ্ঞায়িত করুন যে save চলাকালীন এডিট মানে কি (ফিল্ড লক, আরেকটি সেভ কিউ করা, বা আবার dirty চিহ্নিত করা)।
ব্যর্থ হলে কখনই ব্যবহারকারীর ইনপুট মুছবেন না। ক্ষেত্র-নির্দিষ্ট সমস্যাগুলো সংশ্লিষ্ট ফিল্ডে ইনলাইন দেখান, ফর্ম-লেভেল ব্লকারগুলো সাবমিট এর কাছে রাখুন, এবং নেটওয়ার্ক ব্যর্থতাকে রিট্রাই-যোগ্য রাখুন যাতে একই পে-লোড পুনরায় পাঠানো যায় যদি ব্যবহারকারী কিছু না বদলে।
এক-বারের ইভেন্টগুলো আপনার স্থায়ী state-এ রাখবেন না। MVVM-এ এগুলো আলাদা স্ট্রিম (যেমন একটি SharedFlow) দিয়ে পাঠান, এবং MVI-তে এগুলো Effects হিসেবে মডেল করুন যাতে UI একবার গ্রহণ করে। এতে রোটেশান বা পুনরায় সাবস্ক্রাইব করা হলে ডুপ্লিকেট snackbar বা নেভিগেশন এড়ানো যায়।
প্রধানত কাঁচা ব্যবহারকারী ইনপুট (ইউজার যেভাবে টাইপ করেছে সেই স্ট্রিং, নির্বাচিত IDs, অ্যাটাচমেন্ট URI) এবং পরে সেফলি মার্জ করার জন্য পর্যাপ্ত মেটাডেটা (একটি last-known server snapshot এবং একটি ভার্সন মার্কার) সেভ করুন। ভ্যালিডেশন রিস্টোরে পুনরায় গণনা করুন—ভ্যালিডেশন নিজেরাই সেভ করা না করে। একটি সহজ স্কিমা ভার্সন যোগ করুন যাতে অ্যাপ আপডেটে রিস্টোর ভাঙলে মাইগ্রেশন করা যায়।
একটি ছোট ডিবাউন্স (প্রায় 300–800 ms) ব্যবহার করুন এবং ধাপে পরিবর্তন বা ব্যাকগ্রাউন্ডে যাওয়ার সময় সেভ করুন। প্রতিটি কীস্ট্রোক-এ সেভ করা শব্দমাত্রা বাড়ায় এবং কনটেনশন তৈরি করে, আর শুধুই এক্সিটে সেভ করলে প্রসেস ডেথ বা ব্যাহত হলে কাজ হারিয়ে যেতে পারে।
উভয় সাইডেই ভার্সন মার্কার রাখুন (যেমন updatedAt, ETag, বা লোকাল ইনক্রিমেন্ট)। যদি সার্ভার ভার্সন অপরিবর্তিত থাকে, ড্রাফ্ট প্রয়োগ করে সাবমিট করুন; যদি বদলায়, ব্যবহারকারীকে স্পষ্ট অপশন দেখান: আমার ড্রাফ্ট রাখুন না সার্ভার ডেটা রিলোড করুন—চুপিসারে ওভাররাইট করবেন না।


