Supreme Seed Power+
কৃষি ক্ষেত্র কর্মকর্তাদের জন্য একটা অফলাইন-ফার্স্ট React Native field-operations অ্যাপ — GPS-ট্যাগড crop program, dealer registration, আর এমন একটা sync layer যা production না ভেঙে একটা বড় native SDK upgrade টিকে থাকার মতো যথেষ্ট মজবুত।
সংক্ষেপ
Supreme Seed Power+ হলো Surovi Group-এর বীজ ব্যবসার (HeeraBEEJ) জন্য একটা field-operations অ্যাপ, field officer-রা (FPF) যা ব্যবহার করে GPS-ট্যাগড crop program চালাতে আর নতুন dealer/customer অ্যাকাউন্ট রেজিস্টার করতে — এমন সব গ্রামীণ এলাকা থেকে, যেখানে নেটওয়ার্ক থাকাটা জরুরি না। ফিল্ড-ওয়ার্কার ইউজারবেসের জন্য অ্যাপটা পুরোপুরি বাংলা, আর কানেকশন পাওয়া গেলেই একটা Laravel Sanctum backend-এর সাথে sync করে।
ইঞ্জিনিয়ারিং সমস্যাটা ফর্ম পূরণ করা না — সমস্যা হলো প্রতিটা লেখাকে এমনভাবে বানানো যাতে সেটা অজানা সময়ের অফলাইন পিরিয়ড টিকে থাকতে পারে, কোনো ডেটা না হারিয়ে বা দুইবার সাবমিট না হয়ে, অথচ চারপাশের native টুলচেইন (Expo, React Native, MMKV, React Navigation) ক্রমাগত বদলাতে থাকে এমন একটা production অ্যাপের নিচে, যেখানে downtime নেওয়ার সুযোগ নেই।
মোবাইল অ্যাপ্লিকেশনটা আমি end-to-end ওউন করি: architecture, offline-sync design, native build pipeline, আর চলমান ফিচার ডেলিভারি।
সমস্যাটা কী ছিল
ফিল্ড অফিসাররা এমন এলাকায় কাজ করে যেখানে মোবাইল সিগন্যাল অনির্ভরযোগ্য বা একেবারেই নেই, কিন্তু তাদের দৈনন্দিন রুটিনের অংশ হিসেবে GPS-ট্যাগড crop program ডেটা ক্যাপচার করতে হয় আর নতুন dealer/customer রেজিস্টার করতে হয়। একটা web dashboard বা online-only অ্যাপ কার্যকর না — কোনো গ্রামে কভারেজ ছিল না বলে একজন অফিসারের একটা ফর্ম হারানো মেনে নেওয়া যায় না, আর কানেক্টিভিটি ফিরলে backend-ও ডুপ্লিকেট বা আংশিক সাবমিশন গ্রহণ করতে পারে না।
অফলাইন constraint-এর উপরে, দ্রুত-পরিবর্তনশীল একটা native ইকোসিস্টেমের মধ্য দিয়েও অ্যাপটাকে ship হতে হয়: Expo আর React Native দুটোই বছরে একাধিকবার breaking change আনে, আর Android-এর targetSdk রিকোয়্যারমেন্ট নিয়মিত upgrade বাধ্যতামূলক করে দেয়, যা ঐচ্ছিক না। ফিল্ড স্টাফরা প্রতিদিন ব্যবহার করে এমন একটা production অ্যাপে এমন কোনো upgrade-এর সহনশীলতা প্রায় নেই যেটা নীরবে auth, native module, বা build pipeline ভেঙে দেয়।
আমার ভূমিকা
- অফলাইন-ফার্স্ট sync architecture (queue, dual-phase upload, status lifecycle) শুরু থেকে ডিজাইন আর ইমপ্লিমেন্ট করেছি
- native build pipeline ওউন করি: EAS build, Android signing, আর SDK/React Native upgrade path
- একটা backend স্পেসিফিকেশনের বিরুদ্ধে customer-registration API কন্ট্র্যাক্টের একটা পূর্ণাঙ্গ field-by-field reconciliation লিড করেছি, প্রয়োজনীয় type আর UI পরিবর্তন ship করেছি
- Android API 36 (targetSdk 36) Play Store compliance-এর জন্য প্রয়োজনীয় একটা multi-layered native dependency upgrade (Expo SDK, React Native, MMKV, React Navigation) diagnose আর resolve করেছি
- একটা platform-level Android IME input বাগ একাধিক ইঞ্জিনিয়ারিং iteration-এর মাধ্যমে investigate আর characterize করেছি, ভবিষ্যতের রেফারেন্সের জন্য root cause analysis ডকুমেন্ট করেছি
কারিগরি স্ট্যাক
| Layer | Choice |
|---|---|
| Framework | React Native 0.81 (New Architecture) via Expo SDK 54 |
| Routing | Expo Router (file-based navigation) |
| UI | React Native Paper (Material Design 3) |
| State | Zustand |
| Offline storage | MMKV (Nitro Modules), dual-instance design for auth vs. domain data |
| Networking | Axios, with a custom token-refresh interceptor chain |
| Backend | Laravel + Sanctum token auth (external, consumed via REST) |
| Build / Distribution | EAS Build (Android APK/AAB), Gradle |
| Language | TypeScript (mixed with legacy JS files being migrated incrementally) |
কারিগরি সিদ্ধান্ত
Offline-First Data Flow
program-creation আর customer-registration দুইটা flow-ই জিরো connectivity নিয়ে কাজ করার জন্য ডিজাইন করা:
- ফিল্ড অফিসার ডেটা ক্যাপচার করে — GPS location, ছবি, ফর্ম ফিল্ড — সম্পূর্ণ অফলাইনে।
- রেকর্ডগুলো MMKV-তে persist করা একটা Zustand store-এ (
programStore.ts,customerStore.ts) লোকালি queue হয়, প্রতিটা আইটেমstatus: 'offline' | 'online'ট্যাগড। - একটা শেয়ার্ড
UploadModalকম্পোনেন্ট connectivity ডিটেক্ট করে (hooks/useOnlineStatus.ts) আর একটা two-phase sync চালায়: প্রথমে একটা নির্দিষ্ট endpoint দিয়ে ছবি আপলোড হয়, তারপর server-returned image path বসিয়ে পুরো রেকর্ড post হয়। - সফলভাবে sync হওয়া রেকর্ড
status: 'online'-এ পাল্টে যায় আর অ্যাপের ভেতরে দৃশ্যমান/এডিটযোগ্য (read-only) থাকে — সাবমিশনের পর কিছুই বাদ যায় না, অফিসারদের জন্য একটা টেকসই লোকাল audit trail রেখে দেয়।
Multi-Step Form State Management
Customer registration একটা 4-স্টেপ wizard — personal info, address, business details, attachments — একটা একক Zustand store দিয়ে ব্যাকড, যেটা প্রতি-স্টেপ partial state ধরে রাখে, submission-এর সময় একটা পূর্ণাঙ্গ payload-এ merge হয়। প্রতিটা স্ক্রিন স্বাধীনভাবে validate আর persist করে, যা mid-flow exit/resume আর submission-পরবর্তী edit/read-only review সাপোর্ট করে, একটা ভারী form লাইব্রেরি না এনেই।
Dual MMKV Instance Design
দুইটা আলাদা MMKV configuration ইচ্ছাকৃতভাবে আলাদা রাখা হয়েছে: একটা auth/session ডেটার জন্য (token, hydration state), একটা domain/business ডেটার জন্য (offline queue)। এটা blast radius আলাদা রাখে — একটা clear বা migrate করলে অন্যটা নষ্ট হওয়ার ঝুঁকি কখনো থাকে না — আর cold start-এর সময় business ডেটা থেকে স্বাধীনভাবে auth layer hydrate হতে দেয়।
Auth Hydration Gate
protected route layout-এ একটা নির্দিষ্ট gate MMKV → Zustand hydration সম্পূর্ণ না হওয়া পর্যন্ত child স্ক্রিন render আটকে রাখে। এটা একটা আসল race condition বন্ধ করে, যেখানে token লোড হওয়ার আগেই tab screen-এর effect ফায়ার হতো, মাঝেমধ্যে 401 তৈরি করত — persisted-state React Native অ্যাপে সাধারণ এমন একটা সূক্ষ্ম বাগ ক্লাস, যা স্পষ্ট একটা gate ছাড়া মিস করা সহজ।
Resilient API Layer
Axios ক্লায়েন্টে দুইটা স্বাধীন recovery path layered করা: একটা request interceptor, যেটা Zustand store এখনো hydrate না হলে সরাসরি MMKV থেকে token পড়ায় fallback করে, আর একটা response interceptor, যেটা 401-এ একবার retry করে storage থেকে token পুনরায় পড়ে — store সম্পূর্ণ বাইপাস করে — loop ঠেকাতে একটা retry flag দিয়ে গার্ড করা। এটা অ্যাপকে store/storage-এর timing skew-এর বিরুদ্ধে resilient রাখে, call site জুড়ে ম্যানুয়াল retry logic না ছড়িয়েই।
ফিল্ড কন্ডিশন ও UX
- পূর্ণাঙ্গ বাংলা UI ফিল্ড-ওয়ার্কার ইউজারবেসের জন্য, সাথে বাংলা-নিরাপদ date formatting (backend ইনজেস্ট করতে পারে না এমন locale-নির্ভর numeral rendering এড়িয়ে) আর সীমিত UI কন্টেইনারের ভেতরে বাংলা glyph metric-এর জন্য font-rendering ফিক্স।
- ফিল্ড কন্ডিশনের জন্য অপ্টিমাইজ করা GPS ক্যাপচার: তাৎক্ষণিক ফিডব্যাকের জন্য প্রথমে last-known-location চেষ্টা করে, তারপর একটা fresh low-accuracy fix-এ fallback করে, গ্রামীণ GPS সিগন্যালের তারতম্যের বিপরীতে গতি ব্যালেন্স করে।
- OS ক্যামেরা intent-এর বদলে in-app ক্যামেরা ক্যাপচার, Android 14/15-এর FileProvider/intent অসামঞ্জস্য এড়াতে, যা অফিসারদের আসল ডিভাইসে অনির্ভরযোগ্য photo capture ঘটাতো।
- আপলোডের আগে client-side image compression, কারণ ফিল্ড অফিসাররা নিয়মিত low-signal এলাকায় সীমিত মোবাইল ডেটায় কাজ করে।
চ্যালেঞ্জ ও সমাধান
চ্যালেঞ্জ: Android API 36 compliance-এর জন্য একটা বড় native SDK upgrade (Expo 53 → 54 / RN 0.79 → 0.81) প্রয়োজন ছিল, আর এটা একটা রুটিন dependency bump-এর চেয়ে অনেক বেশি কিছু সামনে এনেছিল।
সমাধান: প্রতিটা ভাঙন আড়াল না করে নিজের শর্তে সমাধান করেছি: react-native-mmkv v3-এর C++ codegen RN 0.81-এর C++20 bridging header-এর বিরুদ্ধে কম্পাইল বন্ধ করার পর সেটা থেকে migrate করেছি, v4-এর Nitro Modules rewrite আর তার createMMKV()/.remove() API গ্রহণ করেছি; কাজ করা file-handling logic পুনরায় না লিখে expo-file-system-এর API-shape পরিবর্তনকে তার /legacy compatibility import দিয়ে ঘুরিয়ে দিয়েছি; @react-navigation/* প্যাকেজ জুড়ে (root node_modules বনাম Expo Router-এর bundled copy) runtime-এ NavigationContent ক্র্যাশ করা একটা duplicate-module resolution বাগ diagnose করেছি, dependency range পুনরায় সাজিয়ে ফিক্স করেছি; আর চিহ্নিত করেছি কোন hand-maintained Android customization — release keystore, signing credential, একটা manifest permission suppression — expo prebuild --clean-এ টিকে থাকে না, release-signing configuration না হারিয়ে সেগুলো পদ্ধতিগতভাবে পুনরুদ্ধার করেছি।
চ্যালেঞ্জ: সত্যিকারের একটা জটিল Android input বাগ — একটা controlled TextInput-এ টাইপ করার সময় upper-case transformation জোর করার ফলে অক্ষর duplicate হয়ে যেত, input method বদলানোর ঠিক পরে সবচেয়ে বেশি দেখা যেত।
সমাধান: এটাকে JS-driven value rewrite আর native keyboard-এর predictive-text engine-এর মাঝে composing-region conflict পর্যন্ত ট্রেস করেছি। ফিচারটা descope হওয়ার আগে আসল root cause characterize করতে বেশ কয়েকটা mitigation strategy — native autoCapitalize, blur-time normalization, autocorrect/spellcheck বন্ধ করা, non-predictive keyboard type জোর করা — দিয়ে iterate করেছি, শুধু লক্ষণ ঢেকে রাখা একটা fix ship করার বদলে।
চ্যালেঞ্জ: একটা ফিচার আগেই ship হয়ে যাওয়ার পর একটা authoritative backend field specification এসেছিল, কী কী drift হয়েছে তার কোনো স্পষ্ট তালিকা ছাড়াই।
সমাধান: লাইভ implementation-এর বিরুদ্ধে একটা পদ্ধতিগত field-by-field diff চালিয়েছি — type, store, আর তিনটা UI স্ক্রিন — একটা মিসিং field, ছয়টা মিসিং localized companion field, আর বেশ কয়েকটা TypeScript nullability অমিল সামনে এনেছি, আর ফিচারটা শুরু থেকে পুনরায় না বানিয়েই gap বন্ধ করেছি।
ফলাফল
- Offline queue আর dual-phase sync মানে একজন ফিল্ড অফিসারের কাজ যেকোনো দৈর্ঘ্যের connectivity গ্যাপ টিকে থাকে, শূন্য ডুপ্লিকেট সাবমিশন বা নীরব ডেটা হারানো ছাড়াই
- native dependency গ্রাফ জুড়ে চারটা স্বাধীন breaking change সত্ত্বেও Expo 54 / RN 0.81 upgrade পরিষ্কারভাবে ship হয়েছে, অ্যাপকে Android-এর targetSdk 36 রিকোয়্যারমেন্টের সাথে compliant রেখে
- IME duplicate-character বাগের একটা ডকুমেন্টেড root-cause investigation পরবর্তী যেকোনো ইঞ্জিনিয়ারের জন্য একটা স্পষ্ট রেকর্ড রেখে গেছে, যে একই রকম composing-region সমস্যায় পড়বে
- Field-by-field API reconciliation একটা আসল contract drift বন্ধ করেছে, কোনো ফিচার পুনরায় না বানিয়েই
- Google Play-এ লাইভ, v2.1.4, সক্রিয় ডেভেলপমেন্টে