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

2026 में Android पर लोकल-फर्स्ट डेटा का बैकअप: SAF एक्सपोर्ट, Auto Backup, और एक ऐसा रीस्टोर फ़्लो जिस पर भरोसा किया जा सके

बिना सर्वर के लोकल-फर्स्ट Android ऐप्स यूज़र डेटा का बैकअप कैसे लेते हैं: SAF एक्सपोर्ट/इम्पोर्ट, Auto Backup for App Data, और एक रीस्टोर जो ओवरराइट करने से पहले जाँच करता है।

MFKAPPS 5 मिनट पढ़ना

“कोई सर्वर नहीं” ही एक लोकल-फ़र्स्ट ऐप की पूरी पिच है। यही वजह भी है कि एक फ़ैक्ट्री रीसेट, खोया हुआ फ़ोन, या डिवाइस अपग्रेड, Granyn में महीनों की बजट हिस्ट्री मिटा सकता है — बिना किसी ऐसी जगह के जहाँ से उसे वापस लाया जा सके। अगर कोई क्लाउड नहीं है, तो बैकअप को बाद में सोचने वाली चीज़ नहीं बनाया जा सकता — यह एक फ़ीचर होना चाहिए, ठीक उसी सावधानी के साथ बनाया गया जितनी उस डेटा मॉडल को बनाने में लगी थी जिसे यह सुरक्षित करता है।

Android पर यहाँ दो तरीक़े मायने रखते हैं, और दोनों अलग-अलग समस्याएँ हल करते हैं। कोई भी दूसरे की जगह नहीं ले सकता।

Auto Backup for App Data: ऑटोमैटिक, लेकिन अपारदर्शी

अगर आप ऑप्ट-इन करते हैं, तो Android आपके ऐप की फ़ाइलों का बैकअप यूज़र के Google Drive में अपने-आप, मुफ़्त में ले लेता है:

<!-- AndroidManifest.xml -->
<application
    android:allowBackup="true"
    android:fullBackupContent="@xml/backup_rules"
    ...>
<!-- res/xml/backup_rules.xml -->
<full-backup-content>
    <include domain="database" path="granyn.db" />
    <exclude domain="database" path="granyn.db-wal" />
    <exclude domain="sharedpref" path="device_keys.xml" />
</full-backup-content>

यह सच में उपयोगी है — यह उस व्यक्ति के लिए सेफ़्टी नेट है जो कभी सेटिंग्स स्क्रीन खोलता ही नहीं। लेकिन इसकी कुछ असली सीमाएँ हैं, जिनसे लड़ने के बजाय मैं उनके इर्द-गिर्द योजना बनाता हूँ:

  • कुल 25MB, और इससे ऊपर जाने पर चुपचाप छाँट दिया जाता है। सालों की ट्रांज़ैक्शन हिस्ट्री और स्क्रीनशॉट्स वाला Room डेटाबेस अंदाज़े से कहीं जल्दी इस सीमा तक पहुँच सकता है।
  • यह सिर्फ़ नए डिवाइस के आउट-ऑफ़-बॉक्स सेटअप फ़्लो के दौरान अपने-आप रीस्टोर होता है, वह भी तब जब वह उसी Google अकाउंट से साइन-इन हो। जो यूज़र उसी फ़ोन पर ऐप फिर से इंस्टॉल करता है, उसे इस तरीक़े से डेटा वापस नहीं मिलता।
  • यह अपारदर्शी है। ऐप के अंदर कोई पुष्टि नहीं मिलती कि क्या और कब बैकअप हुआ। मैं यूज़र को “3 दिन पहले आख़िरी बार बैकअप हुआ” जैसा कुछ नहीं दिखा सकता, क्योंकि OS मुझे यह बताता ही नहीं।

अच्छा डिफ़ॉल्ट, लेकिन ख़राब मुख्य रणनीति। यह उन लोगों के लिए बैकअप है जो कभी बैकअप के बारे में सोचते ही नहीं — यह वह नहीं है जिसकी तरफ़ मैं किसी चिंतित यूज़र को इशारा करूँगा।

स्पष्ट एक्सपोर्ट: वह जिसे यूज़र वाकई नियंत्रित करता है

जिस तरीक़े पर मुझे असल में भरोसा है वह एक सीधा एक्सपोर्ट/इम्पोर्ट है जिसे यूज़र ख़ुद ट्रिगर करता है, Storage Access Framework के ज़रिए अपनी पसंद की किसी भी जगह फ़ाइल लिखते हुए:

@Serializable
data class GranynExport(
    val schemaVersion: Int = 2,
    val exportedAt: Long,
    val accounts: List<AccountDto>,
    val transactions: List<TransactionDto>,
)

suspend fun exportTo(context: Context, uri: Uri) {
    val export = GranynExport(
        exportedAt = clock.nowMillis(),
        accounts = accountDao.getAll().map { it.toDto() },
        transactions = transactionDao.getAll().map { it.toDto() },
    )
    context.contentResolver.openOutputStream(uri)?.use { out ->
        out.write(Json.encodeToString(export).toByteArray())
    }
}

ACTION_CREATE_DOCUMENT से ट्रिगर होता है, इसलिए यूज़र ख़ुद मंज़िल चुनता है — डिवाइस स्टोरेज, कोई USB ड्राइव, या जो भी क्लाउड फ़ोल्डर वह पहले से सिंक करता है। एक बार वाले पिकर के अलावा कोई अनुमति नहीं चाहिए, और फ़ाइल सीधी, जाँची जा सकने वाली JSON होती है। यह आख़िरी बात सुनने में जितनी छोटी लगती है, उससे कहीं ज़्यादा मायने रखती है: जो यूज़र टेक्स्ट एडिटर में बैकअप खोलकर अपने ख़ुद के अकाउंट के नाम देख सकता है, वह उस पर उस तरह भरोसा करता है जो एक अपारदर्शी .bak ब्लॉब कभी नहीं कमा पाता।

schemaVersion वह फ़ील्ड है जो भविष्य के मुझे बचाता है। Granyn का डेटा मॉडल लॉन्च के बाद से एक बार बदल भी चुका है — कोई नई फ़ील्ड जुड़ने का मतलब है कि मैं वर्ज़न बढ़ा देता हूँ, और इम्पोर्टर को पता होता है कि किस शेप की उम्मीद करनी है, बजाय इसके कि जो मौजूद है उससे अंदाज़ा लगाए।

रीस्टोर को कुछ भी ओवरराइट करने का हक़ कमाना पड़ता है

इम्पोर्ट वह जगह है जहाँ बैकअप फ़ीचर की असल परीक्षा होती है, और यही वह हिस्सा है जिसके बारे में सतर्क रहना ज़रूरी है। यहाँ फ़ेल्योर का तरीक़ा “फ़ाइल करप्ट है” नहीं है — यह है कि “फ़ाइल वैध है, लेकिन एक आधी-अधूरी राइट यूज़र को शुरुआत से भी कम डेटा के साथ छोड़ देती है।” जिन नियमों पर मैं रीस्टोर को कसता हूँ:

  1. लाइव डेटाबेस को छूने से पहले पार्स और वैलिडेट करें। schemaVersion जाँचें, देखें कि फ़ाइल खाली तो नहीं, और यह भी देखें कि रेफ़रेंस किए गए IDs आपस में सुसंगत हैं।
  2. कमिट करने से पहले एक सारांश दिखाएँ। “12 अकाउंट, 340 ट्रांज़ैक्शन, 24 जुलाई को बैकअप हुआ” — एक ऐसा आँकड़ा जिसे यूज़र कुछ भी ओवरराइट होने से पहले अपनी याद्दाश्त से मिलाकर परख सके।
  3. राइट को एक ही Room ट्रांज़ैक्शन में लपेटें। या तो पूरा इम्पोर्ट हो, या बिल्कुल न हो; इम्पोर्ट के बीच में हुआ क्रैश डेटाबेस को अधूरी, जुड़ी-टूटी हालत में नहीं छोड़ सकता।
suspend fun restoreFrom(export: GranynExport) = db.withTransaction {
    require(export.schemaVersion <= CURRENT_SCHEMA_VERSION) {
        "Backup was made with a newer app version"
    }
    accountDao.deleteAll()
    transactionDao.deleteAll()
    accountDao.insertAll(export.accounts.map { it.toEntity() })
    transactionDao.insertAll(export.transactions.map { it.toEntity() })
}

असली सुरक्षा का काम db.withTransaction कर रहा है — अगर insertAll बीच में कहीं थ्रो करे, तो Room पूरे ब्लॉक को वापस रोल कर देता है और यूज़र का मौजूदा डेटा अछूता रहता है। इसके बिना, आधे में फ़ेल होने वाला रीस्टोर, रीस्टोर फ़ीचर के बिल्कुल न होने से भी बुरा है।

इसे सेटिंग्स की एक लाइन नहीं, बल्कि एक मुख्य फ़ीचर की तरह समझें

एक्सपोर्ट/इम्पोर्ट को Settings में दबे एक अकेले बटन के रूप में शिप करके काम ख़त्म समझ लेना आसान लगता है। एक लोकल-फ़र्स्ट ऐप के लिए, वह बटन ही है डिज़ास्टर रिकवरी प्लान — इसमें किसी बग को चुपचाप ढँकने वाला कोई सर्वर-साइड बैकअप नहीं है। मैं रीस्टोर पाथ को ठीक उसी गंभीरता से टेस्ट करता हूँ जैसे ऑनबोर्डिंग को — असली एक्सपोर्ट की गई फ़ाइलें, लॉक्ड और रीबूट किया गया फ़ोन, एक फ़्रेश इंस्टॉल। अगर एक्सपोर्ट बटन ख़राब है, तो किसी को तब तक पता नहीं चलेगा जब तक उसे इसकी सबसे ज़्यादा ज़रूरत न पड़े — और यही इसे खोजने का सबसे ग़लत वक़्त है।

इसमें से कुछ भी जटिल इंजीनियरिंग नहीं है। यह वह उबाऊ किस्म का काम है जो सिर्फ़ उस एक दिन फल देता है जब यूज़र को इसकी वाक़ई ज़रूरत पड़ती है — और एक लोकल-फ़र्स्ट ऐप के लिए, यही तो इसके होने की पूरी वजह है।

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

जर्नल से और भी

MFKAPPS 6 मिनट पढ़ना

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

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

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

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

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

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

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

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

#android #engineering #room