SharedPreferences has long been a staple for storing small pieces of data and user preferences in Android apps. However, it has notable limitations, such as a lack of type safety, no support for safe schema evolution, and potential performance issues on the main thread. Google introduced Proto DataStore as a modern and robust alternative, offering:
Strong typing with Protocol Buffers
Safe schema evolution
Built-in migration support
Flow API for reactive programming
Coroutines support for main-thread safety
In this post, we’ll walk through the process of migrating your existing SharedPreferences data to Proto DataStore without data loss, including best practices and common pitfalls to avoid.
Step 1: Add Dependencies
First, add the necessary dependencies to your app’s build.gradle file:
class UserPreferencesRepository(private val context: Context) {
private val dataStore: DataStore<UserPreferences> = context.createDataStore(
fileName = "user_prefs.pb",
serializer = UserPreferencesSerializer
)
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(UserPreferences.getDefaultInstance())
} else {
throw exception
}
}
suspend fun migrateFromSharedPreferences() = withContext(Dispatchers.IO) {
try {
val sharedPrefs = context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
// Read existing preferences
val userName = sharedPrefs.getString("user_name", "") ?: ""
val notificationsEnabled = sharedPrefs.getBoolean("notifications_enabled", true)
val theme = sharedPrefs.getString("theme", "system") ?: "system"
// Migrate to DataStore
dataStore.updateData { preferences ->
preferences.toBuilder()
.setUserName(userName)
.setNotificationsEnabled(notificationsEnabled)
.setTheme(theme)
.setSchemaVersion(1)
.build()
}
// Verify migration
val migratedData = dataStore.data.first()
if (migratedData.userName == userName &&
migratedData.notificationsEnabled == notificationsEnabled &&
migratedData.theme == theme) {
// Clear SharedPreferences after successful migration
sharedPrefs.edit().clear().apply()
Result.success(Unit)
} else {
Result.failure(Exception("Migration verification failed"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun updateUserName(name: String) {
dataStore.updateData { preferences ->
preferences.toBuilder()
.setUserName(name)
.build()
}
}
suspend fun updateNotificationsEnabled(enabled: Boolean) {
dataStore.updateData { preferences ->
preferences.toBuilder()
.setNotificationsEnabled(enabled)
.build()
}
}
suspend fun updateTheme(theme: String) {
dataStore.updateData { preferences ->
preferences.toBuilder()
.setTheme(theme)
.build()
}
}
}
Step 5: Use in Your App
Here’s how to use the repository in your app:
1234567891011121314151617181920212223242526
class MainActivity : AppCompatActivity() {
private lateinit var repository: UserPreferencesRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
repository = UserPreferencesRepository(applicationContext)
// Migrate data on app first launch
lifecycleScope.launch {
repository.migrateFromSharedPreferences()
}
// Observe preferences changes
lifecycleScope.launch {
repository.userPreferencesFlow.collect { preferences ->
// Update UI based on preferences
updateUI(preferences)
}
}
}
private fun updateUI(preferences: UserPreferences) {
// Update your UI components based on preferences
}
}
Testing Your Migration
Here’s an example of how to test your migration:
123456789101112131415161718192021222324252627
@Test
fun testMigrationFromSharedPreferences() = runTest {
val context = ApplicationProvider.getApplicationContext<Context>()
// Set up test SharedPreferences
val sharedPrefs = context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
sharedPrefs.edit()
.putString("user_name", "Test User")
.putBoolean("notifications_enabled", true)
.putString("theme", "dark")
.apply()
// Perform migration
val repository = UserPreferencesRepository(context)
val result = repository.migrateFromSharedPreferences()
// Verify migration
assert(result.isSuccess)
val migratedData = repository.userPreferencesFlow.first()
assertEquals("Test User", migratedData.userName)
assertTrue(migratedData.notificationsEnabled)
assertEquals("dark", migratedData.theme)
// Verify SharedPreferences was cleared
assertTrue(sharedPrefs.all.isEmpty())
}
Best Practices and Common Pitfalls
Schema Evolution: When adding new fields to your proto schema, always:
Use new field numbers
Provide default values
Increment the schema version
Error Handling: Always handle potential exceptions when reading/writing data:
IOException for file system issues
CorruptionException for invalid data
InvalidProtocolBufferException for proto parsing errors
Performance:
Perform migrations in the background using Coroutines
Use Flow’s collectLatest when frequent updates aren’t necessary
Consider caching frequently accessed values
Testing:
Write unit tests for your repository
Test migration with various SharedPreferences states
Include error cases in your tests
Test schema evolution scenarios
Conclusion
Migrating from SharedPreferences to Proto DataStore requires some initial setup, but the benefits of type safety, schema evolution, and reactive programming make it worthwhile. The resulting code will be more maintainable, type-safe, and resistant to runtime errors. Happy coding!