2026 में Jetpack DataStore: बिना एक भी सेटिंग खोए SharedPreferences से माइग्रेट करना
Android पर SharedPreferences से Jetpack DataStore पर जाने की एक व्यावहारिक गाइड — असिंक्रोनस दिक्कतें, मौजूदा वैल्यू को बचाने वाला माइग्रेशन तरीका, और इसे कैसे टेस्ट करें।
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 जोड़ा गया है, एक टेस्ट पुराना फ़ॉर्मैट तैयार करता है और सत्यापित करता है कि हर वैल्यू बच गई, और जब तक टेलीमेट्री यह पुष्टि नहीं कर देती कि माइग्रेशन इंस्टॉल्ड बेस पर चल चुका है, तब तक पुरानी फ़ाइल को छुआ नहीं जाता। यह उस बदलाव के लिए थोड़ी ज़्यादा सावधानी है जिसे यूज़र्स को बिल्कुल भी नोटिस नहीं करना चाहिए — जो कि एक सेटिंग्स माइग्रेशन के लिए बिल्कुल यही मकसद है।
// संबंधित पठन
जर्नल से और भी
2026 में WorkManager: Android पर यूनीक वर्क, चेनिंग और बैकग्राउंड जॉब्स की टेस्टिंग
Android पर WorkManager की एक प्रैक्टिकल गाइड — यूनीक वर्क पॉलिसी, चेनिंग, एक्सपेडाइटेड वर्क, और शिप करने से पहले बैकग्राउंड जॉब को असल में कैसे टेस्ट करें।
2026 में Android App Links: आपके https:// लिंक अब भी ब्राउज़र में क्यों खुलते हैं
Digital Asset Links वेरिफिकेशन, assetlinks.json में छिपी गड़बड़ियाँ, और वो adb कमांड्स जो बताते हैं कि Android लिंक आपकी ऐप को क्यों नहीं दे रहा।
Android का In-App Review API: बिना परेशान किए रेटिंग माँगना
Google के In-App Review API की एक व्यावहारिक गाइड — यह असल में कैसे काम करता है, इसे कब ट्रिगर करना चाहिए, और सामान्य 'हमें रेट करें' पॉपअप आपकी Play Store रेटिंग को चुपचाप कैसे नुकसान पहुँचा रहा है।