Skip to content

Instantly share code, notes, and snippets.

@tprochazka
Last active November 12, 2025 15:47
Show Gist options
  • Select an option

  • Save tprochazka/e1966db92a832f37f2ebfd227e35ee28 to your computer and use it in GitHub Desktop.

Select an option

Save tprochazka/e1966db92a832f37f2ebfd227e35ee28 to your computer and use it in GitHub Desktop.
DataStore migration function from one DataStore to the new one with possibility to change key names
/**
* Creates a DataStore-to-DataStore migration that optionally remaps keys from old names to new names.
*
* Useful when migrating from a legacy DataStore to a new unified DataStore.
* Unlike [SharedPreferencesMigration], this can read from another DataStore (Protocol Buffer format).
*
* This migration creates a temporary DataStore instance to read the legacy data, then closes it
* automatically after migration completes. This prevents keeping unused DataStore instances open.
*
* @param context The application context
* @param legacyDataStoreName Name of the legacy DataStore file (without path or extension)
* @param keyMapping Map of old key names to new key names (oldKey -> newKey).
* If oldKey == newKey, the key is copied as-is.
*
* Example:
* ```
* DataStoreMigration(
* context = context,
* legacyDataStoreName = "dataStorePref.AppLaunchCount",
* keyMapping = mapOf(
* "app_launch_count" to "app_launch_count", // No rename
* "old_key_name" to "new_key_name" // With rename
* )
* )
* ```
*/
@Suppress("FunctionName", "SameParameterValue")
private fun DataStoreMigration(
context: Context,
legacyDataStoreName: String,
keyMapping: Map<String, String>
): DataMigration<Preferences> {
return object : DataMigration<Preferences> {
override suspend fun shouldMigrate(currentData: Preferences): Boolean {
// Check if legacy DataStore file exists
val legacyFile = File(context.filesDir, "datastore/$legacyDataStoreName.preferences_pb")
if (!legacyFile.exists()) {
return false
}
// Migrate if any of the new keys don't exist yet
return keyMapping.values.any { newKey ->
// Check all possible types - if none exist, we should migrate
currentData[intPreferencesKey(newKey)] == null &&
currentData[longPreferencesKey(newKey)] == null &&
currentData[stringPreferencesKey(newKey)] == null &&
currentData[booleanPreferencesKey(newKey)] == null &&
currentData[floatPreferencesKey(newKey)] == null
}
}
override suspend fun migrate(currentData: Preferences): Preferences {
// Create a temporary DataStore to read legacy data
val legacyDataStore = PreferenceDataStoreFactory.create(
produceFile = { context.preferencesDataStoreFile(legacyDataStoreName) }
)
val legacyData = legacyDataStore.data.first()
return currentData.toMutablePreferences().apply {
keyMapping.forEach { (oldKey, newKey) ->
// Try each type - DataStore stores typed keys
legacyData[intPreferencesKey(oldKey)]?.let {
this[intPreferencesKey(newKey)] = it
}
legacyData[longPreferencesKey(oldKey)]?.let {
this[longPreferencesKey(newKey)] = it
}
legacyData[stringPreferencesKey(oldKey)]?.let {
this[stringPreferencesKey(newKey)] = it
}
legacyData[booleanPreferencesKey(oldKey)]?.let {
this[booleanPreferencesKey(newKey)] = it
}
legacyData[floatPreferencesKey(oldKey)]?.let {
this[floatPreferencesKey(newKey)] = it
}
legacyData[stringSetPreferencesKey(oldKey)]?.let {
this[stringSetPreferencesKey(newKey)] = it
}
}
}.toPreferences()
// Note: The temporary DataStore will be garbage collected after migration
}
override suspend fun cleanUp() {
// We leave the original DataStore intact for safety
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment