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

2026 में Jetpack DataStore: बिना एक भी सेटिंग खोए SharedPreferences से माइग्रेट करना

Android पर SharedPreferences से Jetpack DataStore पर जाने की एक व्यावहारिक गाइड — असिंक्रोनस दिक्कतें, मौजूदा वैल्यू को बचाने वाला माइग्रेशन तरीका, और इसे कैसे टेस्ट करें।

MFKAPPS 6 मिनट पढ़ना

SharedPreferences अब भी काम करता है। लेकिन यह पहली बार एक्सेस होने पर डिस्क से सिंक्रोनस रीड भी करता है, इसमें कंपाइल-टाइम टाइप सेफ्टी नहीं है, और बिना हाथ से लिसनर जोड़े किसी वैल्यू में बदलाव देखने का कोई तरीका नहीं देता। Jetpack DataStore इन तीनों को ठीक करता है, और 2026 में यह वह डिफ़ॉल्ट है जिसे Google किसी भी एक बार इस्तेमाल होने वाले फ्लैग से आगे की हर चीज़ के लिए सुझाता है। ज़्यादातर माइग्रेशन को जो चीज़ रोकती है वह नया API सीखना नहीं है — वह है मौजूदा यूज़र्स को अगले अपडेट में उनकी सेटिंग्स रीसेट किए बिना नए स्टोरेज पर ले जाना।

SharedPreferences में असल में क्या गलत है

getSharedPreferences().getString(...) सिंक्रोनस और सस्ता दिखता है, लेकिन किसी प्रोसेस में पहली बार एक्सेस होने पर यह कॉलिंग थ्रेड को एक असली फ़ाइल रीड पर ब्लॉक कर सकता है — अक्सर ऐप स्टार्टअप के दौरान मेन थ्रेड को। बदलावों को एक स्ट्रीम की तरह इकट्ठा करने का कोई बिल्ट-इन तरीका नहीं है; आपको एक OnSharedPreferenceChangeListener रजिस्टर करना पड़ता है और उसका लाइफ़साइकल खुद मैनेज करना पड़ता है। और हर रीड स्ट्रिंग-आधारित है: की के नाम में एक टाइपो बिना किसी दिक्कत के कंपाइल हो जाता है और रनटाइम पर चुपचाप डिफ़ॉल्ट वैल्यू लौटाकर फेल हो जाता है।

इनमें से कुछ भी कोई असामान्य एज केस नहीं है। यह SharedPreferences का सामान्य व्यवहार है, और Preferences DataStore को खास तौर पर इन्हें बिना मेंटल मॉडल को बहुत बदले ठीक करने के लिए बनाया गया था।

Preferences DataStore: वही आकार, सुरक्षित तरीके से

Preferences DataStore का एक इंस्टेंस एक बार बनाया जाता है, आमतौर पर Context पर एक एक्सटेंशन प्रॉपर्टी के रूप में:

val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
    name = "settings"
)

रीड्स एक Flow के रूप में वापस आती हैं, इसलिए आपको खुद बनाने के बजाय मुफ्त में बदलाव की सूचनाएं मिल जाती हैं:

val TIMER_MINUTES = intPreferencesKey("timer_minutes")

val timerMinutes: Flow<Int> = context.settingsDataStore.data
    .map { prefs -> prefs[TIMER_MINUTES] ?: 25 }

राइट्स एक suspend फ़ंक्शन से होकर गुज़रती हैं, इसलिए वे कभी गलती से मेन थ्रेड से कॉल नहीं होतीं:

suspend fun setTimerMinutes(context: Context, minutes: Int) {
    context.settingsDataStore.edit { prefs ->
        prefs[TIMER_MINUTES] = minutes
    }
}

यह SharedPreferences के काफ़ी करीब है कि रीराइट खुद ही यांत्रिक हो जाता है। पूरा जोखिम इस बात में है कि यूज़र के डिवाइस पर पहले से मौजूद डेटा का क्या होता है।

जो हिस्सा लोग छोड़ देते हैं: मौजूदा वैल्यू को माइग्रेट करना

DataStore बिल्कुल इसी के लिए एक SharedPreferencesMigration देता है, और इसे मिस करना आसान है क्योंकि इसे इस्तेमाल करने के लिए कुछ भी मजबूर नहीं करता — आपका ऐप इसके बिना भी अच्छे से बिल्ड और चलेगा, फिर जिस अपडेट में आप स्टोरेज बैकएंड बदलते हैं उसमें चुपचाप हर सेटिंग को उसके डिफ़ॉल्ट पर रीसेट कर देगा।

val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
    name = "settings",
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration(context, "settings_prefs"))
    }
)

यह माइग्रेशन एक बार चलता है, इस कोड के शिप होने के बाद जब DataStore पहली बार खुलता है: यह पुरानी SharedPreferences फ़ाइल पढ़ता है, हर एंट्री को नए Preferences स्टोर में कॉपी करता है, और पुरानी फ़ाइल को वहीं छोड़ देता है (इसे डिलीट नहीं करता — यह आपका अलग से लिया जाने वाला फ़ैसला है, एक बार जब आप आश्वस्त हो जाएं कि माइग्रेशन हर जगह चल चुका है)। अगर किसी की का टाइप ठीक से मेल नहीं खाता — जैसे एक Set<String> जहां अब आप एक List चाहते हैं — तो आप उसे माइग्रेशन के shouldRunMigration में फ़िल्टर करते हैं या रीड टाइम पर टाइप मिसमैच से एक्सेप्शन आने देने के बजाय उसे साफ़ तौर पर ट्रांसफ़ॉर्म करते हैं।

पुरानी preferences फ़ाइल का नाम गलत लेना — अगर इसे पैकेज-डिफ़ॉल्ट नाम के बजाय किसी कस्टम नाम से बनाया गया था तो यह एक आम गलती है — माइग्रेशन को कॉपी करने के लिए कुछ नहीं मिलता, और हर यूज़र अपडेट पर डिफ़ॉल्ट पर रीसेट हो जाता है। शिप करने से पहले, अंदाज़ा लगाने के बजाय कोडबेस में पहले से मौजूद context.getSharedPreferences("name", MODE_PRIVATE) कॉल्स से सही नाम चेक करें।

जब Preferences DataStore काफ़ी नहीं होता

Preferences DataStore एक फ़्लैट, स्ट्रिंग-आधारित की स्पेस रखता है — इसने थ्रेडिंग और ऑब्ज़र्वेबिलिटी की समस्याएं हल कीं लेकिन टाइप-सेफ्टी की नहीं। Proto DataStore की-वैल्यू के थैले को एक ऐसे स्कीमा से बदल देता है जिसे आप एक बार .proto फ़ाइल में डिफ़ाइन करते हैं, ताकि एक सेटिंग्स ऑब्जेक्ट या तो स्कीमा से मेल खाए या कंपाइल ही न हो — रनटाइम पर टाइपो हुई की से कोई null रिज़ल्ट नहीं आता। जब कोई सेटिंग्स स्क्रीन कुछ फ़्लैग से आगे बढ़ जाए, या जब नेस्टेड ऑब्जेक्ट्स (अपने खुद के साउंड, वाइब्रेशन और क्वाइट-आवर्स फ़ील्ड वाली एक नोटिफ़िकेशन प्रेफ़रेंस) की स्पेस में एक स्ट्रक्चर्ड वैल्यू की बजाय तीन या चार अलग-अलग नेमस्पेस वाली की के रूप में दिखने लगें, तब यह अतिरिक्त सेटअप के लायक है। Mintly में, टाइमर की साउंड, वाइब्रेशन और ऑटो-रीस्टार्ट सेटिंग्स बिल्कुल इसी वजह से एक छोटे Proto DataStore मैसेज में ले जाई गईं — जैसे ही संबंधित सेटिंग्स को साथ में पढ़ने और लिखने की ज़रूरत पड़ती है, एक फ़्लैट की-वैल्यू स्टोर आपके ख़िलाफ़ काम करने लगता है।

नए API की नहीं, माइग्रेशन की टेस्टिंग

नया रीड/राइट कोड इतना सीधा है कि टेस्टिंग छोड़ने का मन कर सकता है। माइग्रेशन ऐसा नहीं है — यह इस बदलाव का वह इकलौता हिस्सा है जो असली यूज़र डेटा पर बिल्कुल एक बार, चुपचाप चलता है, और अगर गलत हो तो दोबारा कोशिश करने का कोई मौका नहीं होता। एक न्यूनतम टेस्ट एक असली SharedPreferences फ़ाइल तैयार करता है, माइग्रेशन जुड़े होने के साथ एक DataStore खोलता है, और सत्यापित करता है कि वैल्यूज़ बच गईं:

@Test
fun migration_preservesExistingTimerSetting() = runTest {
    val prefs = context.getSharedPreferences("settings_prefs", Context.MODE_PRIVATE)
    prefs.edit().putInt("timer_minutes", 45).commit()

    val dataStore = PreferenceDataStoreFactory.create(
        migrations = listOf(SharedPreferencesMigration(context, "settings_prefs")),
        produceFile = { File(context.filesDir, "test_settings.preferences_pb") }
    )

    val minutes = dataStore.data.first()[intPreferencesKey("timer_minutes")]
    assertEquals(45, minutes)
}

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

चेकलिस्ट

SharedPreferences से DataStore माइग्रेशन को मर्ज करने से पहले: पुरानी preferences फ़ाइल का नाम कोडबेस के असली getSharedPreferences() कॉल के मुक़ाबले कन्फ़र्म कर लिया गया है, वर्तमान में इस्तेमाल हो रही हर की के लिए SharedPreferencesMigration जोड़ा गया है, एक टेस्ट पुराना फ़ॉर्मैट तैयार करता है और सत्यापित करता है कि हर वैल्यू बच गई, और जब तक टेलीमेट्री यह पुष्टि नहीं कर देती कि माइग्रेशन इंस्टॉल्ड बेस पर चल चुका है, तब तक पुरानी फ़ाइल को छुआ नहीं जाता। यह उस बदलाव के लिए थोड़ी ज़्यादा सावधानी है जिसे यूज़र्स को बिल्कुल भी नोटिस नहीं करना चाहिए — जो कि एक सेटिंग्स माइग्रेशन के लिए बिल्कुल यही मकसद है।

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

जर्नल से और भी

MFKAPPS 5 मिनट पढ़ना

2026 में androidx.startup: ContentProvider के ढेर के बिना लाइब्रेरी इनिशियलाइज़ेशन का क्रम तय करना

Android की App Startup लाइब्रेरी के लिए एक व्यावहारिक गाइड — initializer को एक ContentProvider में समेटना, dependencies घोषित करना, और वो lazy-init मामले जिनकी जगह यह नहीं ले सकती।

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

2026 में CameraX: कैमरा सेशन लीक किए बिना Preview और ImageAnalysis को बाइंड करना

Android पर CameraX की व्यावहारिक गाइड: Preview और ImageAnalysis को lifecycle से बाइंड करना, सही backpressure रणनीति, और रोटेशन से होने वाली क्रैश।

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

2026 में Android का इन-ऐप अपडेट API: फ़्लेक्सिबल बनाम इमीडिएट, और कब किसे इस्तेमाल करें

Google के इन-ऐप अपडेट API की व्यावहारिक गाइड — फ़्लेक्सिबल बनाम इमीडिएट फ़्लो, अपडेट प्रायोरिटी, स्टेलनेस डेज़, और वह रिज़्यूम चेक जिसे हर कोई भूल जाता है।

#android #engineering #kotlin