सामग्री पर जाएं
सभी पोस्ट

Kotlin, Room और Compose के लिए R8 और ProGuard: वह क्रैश जो सिर्फ़ रिलीज़ में होता है

एक Room + Compose Android ऐप डिबग में बिल्कुल ठीक क्यों चलता है और प्रोडक्शन में क्रैश क्यों होता है, और वे ख़ास R8/ProGuard keep नियम जो इसे यूज़र से पहले पकड़ लेते हैं।

MFKAPPS 5 मिनट पढ़ना

एक बिल्ड जो आपके डिवाइस पर हर टेस्ट पास करता है, फिर बिल्कुल उसी वर्शन पर किसी यूज़र के लिए क्रैश हो जाता है — यह Android के सबसे बेचैन करने वाले बग्स में से एक है, और आमतौर पर इसका एक ही कारण होता है: minifyEnabled true। डिबग बिल्ड shrinking और obfuscation को पूरी तरह छोड़ देते हैं, इसलिए R8 जो कुछ भी छूता, वह तब तक कभी नहीं चलता जब तक रिलीज़ APK — जो पहले से Play Store पर है — उस कोड तक नहीं पहुँचती जिसे आपने असल में कभी चलाया ही नहीं।

यह minification के ख़िलाफ़ कोई तर्क नहीं है। एक Kotlin + Compose ऐप की मेथड काउंट घटाना और बिना इस्तेमाल वाला कोड हटाना, इंस्टॉल साइज़ और कोल्ड स्टार्ट में सचमुच मदद करता है, और Google इसकी उम्मीद बढ़ती जा रही है। सुधार छोटा है: जानिए कि आपके कोडबेस के कौन-से हिस्सों को R8 स्टेटिक रूप से समझ नहीं पाता, और उसे बता दीजिए कि उन्हें छेड़े नहीं।

R8 उन चीज़ों को क्यों तोड़ता है जो “साफ़ तौर पर” काम करती हैं

R8 यह तय करता है कि क्या रखना है, आपके ऐप के एंट्री पॉइंट्स — activities, manifest, वह सब कुछ जो सीधे कॉल होता है — से पहुँच योग्यता ट्रेस करके। वह कोड जिस तक सिर्फ़ reflection से, स्ट्रिंग के रूप में सेव किए गए क्लास नाम से, या कोई लाइब्रेरी रनटाइम पर किसी टाइप को दोबारा बनाकर पहुँचती है, वह इस ट्रेस के लिए अदृश्य होता है। R8 उसका नाम बदल देता है या उसे हटा देता है, ऐप बिना किसी दिक़्क़त के कंपाइल हो जाता है, और फ़ेल्योर तभी सामने आता है जब वह reflective रास्ता असल में चलता है।

दो जगहें जहाँ यह एक सामान्य local-first Android ऐप को व्यवहार में काटती हैं:

नाम से इंस्टैंशिएट होने वाले WorkManager वर्कर्स

अगर आप बैकग्राउंड काम शेड्यूल कर रहे हैं — जैसे Hydrame में रिमाइंडर शेड्यूलिंग के पीछे होता है — तो आपकी ListenableWorker सबक्लासेज़ सीधे कॉल नहीं होतीं। WorkManager वर्कर का पूरा क्लास नाम अपने ख़ुद के डेटाबेस में सेव रखता है और जब काम असल में चलता है — कभी-कभी उसे शेड्यूल करने वाला ऐप प्रोसेस ख़त्म हो चुकने के घंटों बाद — तब Class.forName से इंस्टेंस दोबारा बनाता है। उस क्लास नाम को obfuscate कर दीजिए और क्रैश शेड्यूलिंग के समय नहीं होता — यह बाद में, चुपचाप, WorkManager के dispatcher के अंदर गहराई में एक ClassNotFoundException के रूप में होता है, बिना किसी ऐसे स्टैक ट्रेस के जो आपके कोड की ओर इशारा करे:

# proguard-rules.pro
-keep public class * extends androidx.work.ListenableWorker {
    public <init>(android.content.Context, androidx.work.WorkerParameters);
}

हाल के work-runtime वर्ज़न पहले से ही इसके क़रीब का एक consumer नियम शामिल करते हैं, लेकिन “शायद लाइब्रेरी ने पहले से कवर कर दिया होगा” — यह किसी रिलीज़ बिल्ड में भरोसे पर मान लेने वाली बात नहीं है — मान लेने के बजाय जाँच लीजिए।

बैकअप और एक्सपोर्ट के लिए reflection-आधारित JSON

किसी भी local-first ऐप का असली एक्सपोर्ट/इम्पोर्ट फ़्लो — जैसा Stocky पैंट्री डेटा बैकअप करने के लिए इस्तेमाल करता है — entity data class के फ़ील्ड्स को JSON में और वापस सीरियलाइज़ करता है। अगर यह सीरियलाइज़ेशन किसी reflection-आधारित लाइब्रेरी (Gson, या codegen के बिना Moshi) से होकर गुज़रता है, तो JSON की keys डिफ़ॉल्ट रूप से आपके Kotlin property नाम होती हैं। R8 उन private फ़ील्ड्स का नाम बदल देता है जिन्हें वह बदलने में सुरक्षित समझता है, बनाई गई एक्सपोर्ट फ़ाइल फिर भी सही दिखती है, और फ़ेल्योर सिर्फ़ इम्पोर्ट के समय सामने आता है, जब reflective रीडर उस फ़ील्ड नाम को ढूँढता है जो अब घटी हुई क्लास में मौजूद ही नहीं है। न कोई क्रैश, न कोई एरर — बस डेटा जो चुपचाप वापस नहीं आता।

-keepclassmembers class com.example.app.data.** {
    <fields>;
}
-keepattributes Signature

इसे अपने पूरे कोडबेस पर नहीं, बल्कि अपने असली data पैकेज तक सीमित रखिए — एक व्यापक -keep class **, minify करने का पूरा मक़सद ही ख़त्म कर देता है।

असल में शिप होने वाले बिल्ड को टेस्ट करना

इसे पकड़ने का एकमात्र भरोसेमंद तरीक़ा रिलीज़ बिल्ड चलाना है, डिबग बिल्ड नहीं:

buildTypes {
    release {
        isMinifyEnabled = true
        isShrinkResources = true
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
    }
}

./gradlew assembleRelease चलाइए, APK को किसी असली डिवाइस पर इंस्टॉल कीजिए, और हर सबमिशन से पहले हर मुख्य फ़्लो को हाथ से एक-एक करके देखिए — एक रिमाइंडर शेड्यूल करना, डेटा एक्सपोर्ट करना, फिर ऐप को force-quit करके दोबारा खोलना ताकि यह पुष्टि हो सके कि इम्पोर्ट अब भी उसे पढ़ पा रहा है। यह पाँच मिनट की जाँच है, उस तरह के बग के ख़िलाफ़ जो वरना तब तक अदृश्य रहता जब तक Play Console की क्रैश रिपोर्ट्स भरनी शुरू न हों — किसी थर्ड-पार्टी क्रैश रिपोर्टर के बिना बने ऐप के लिए, बाद में मिलने वाली यही एकमात्र दृश्यता है।

एक और आदत जो रखने लायक़ है: R8 हर रिलीज़ के लिए जो mapping.txt जनरेट करता है, उसे सँभालकर रखिए (Play Console इसे माँगेगा, और App Bundle के ज़रिए अपलोड करने पर यह अपने-आप भी हो जाता है)। इसके बिना, कोई क्रैश जो सचमुच प्रोडक्शन में आप तक पहुँचता है, वह एक-अक्षर वाले क्लास और मेथड नामों से भरा एक स्टैक ट्रेस होता है — R8 के लिए पढ़ने लायक़, आपके लिए नहीं।

निचोड़

Minification शोर मचाकर फ़ेल नहीं होता। यह उसी एक कोड पथ पर फ़ेल होता है जिसे आपने हाथ से दोबारा टेस्ट नहीं किया, उस बिल्ड के शिप होने के हफ़्तों बाद जिसने आपकी जाँची हुई हर चीज़ पास कर दी थी। नाम से इंस्टैंशिएट होने वाली हर क्लास को — वर्कर्स, reflective सीरियलाइज़र्स, वह सब कुछ जिसे कोई लाइब्रेरी स्ट्रिंग से दोबारा बनाती है — एक ऐसे keep नियम की तरह मानिए जिसे आप जानबूझकर लिखते हैं, न कि ऐसे जिसके बारे में आप उम्मीद करते हैं कि किसी लाइब्रेरी के डिफ़ॉल्ट्स आपकी जगह उसे कवर कर लेंगे।

// संबंधित पठन

जर्नल से और भी

MFKAPPS 6 मिनट पढ़ना

2026 में Room TypeConverters: अपने स्कीमा को खराब किए बिना एनम, डेट, और लिस्ट स्टोर करना

Android पर Room TypeConverters के लिए एक व्यावहारिक गाइड — एनम, Instant/LocalDate, और लिस्ट — साथ ही वे ग़लतियाँ जो एक कनवर्टर को एक चुपचाप डेटा-करप्शन बग में बदल देती हैं।

#android #engineering #room
MFKAPPS 5 मिनट पढ़ना

2026 में Room डेटाबेस इंडेक्स: वाकई धीमी क्वेरी को ढूँढना और ठीक करना

Android पर Room/SQLite डेटाबेस को इंडेक्स करने की व्यावहारिक गाइड — EXPLAIN QUERY PLAN पढ़ना, बिना अंदाज़े के @Index जोड़ना, और वे गलतियाँ जो चुपचाप इंडेक्स को बेअसर कर देती हैं।

#android #engineering #room
MFKAPPS 5 मिनट पढ़ना

Room का @Relation: Android पर N+1 क्वेरी के बिना वन-टू-मेनी डेटा क्वेरी करना

Room के @Relation एनोटेशन की एक व्यावहारिक गाइड — categories और entries जैसे वन-टू-मेनी डेटा को N+1 क्वेरी या मैनुअल join के बिना मॉडल करना।

#android #engineering #room