Along time ago I had an Android app as a pet project. And last year I decided to created the iOS version of the application. The best option for me was to use KMP technology since it allowed me to keep the UI part almost as it was, refactor the modules which contain the business logic, replace JVM dependencies with their analogues in pure Kotlin (or implement expect/actual mechanism) and create the UI part for the iOS.
I stored the key-value data in SharedPreferences and I wanted to find some similar solution but for KMP. Luckily, there’s such a library — multiplatform-settings. One of the biggest pros for me was that it can use default SharedPreferences which I already had in the Android app. So, you shouldn’t overthink data migration. Here’s the way how I implemented it.
In common sourceSet:
expect fun getSettings(context: AppContext?): Settings |
In android sourceSet:
actual fun getSettings(context: AppContext?): Settings = SharedPreferencesSettings( | |
PreferenceManager.getDefaultSharedPreferences(context) | |
) |
So, as you can see, Settings is just a wrapper over SharedPreferences in the Android part.
In the iOS part there’s nothing complicated too — Settings wrap old-good NSUserDefaults.
actual fun getSettings(context: AppContext?): Settings = | |
NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults) |
Again, using this library you shouldn’t worry about the data migration, since the app still uses same data sources. But is there any way to store the data in the secure way and still use this library? Sure, and here’s the way how you can do it. In the Android it’s more secure to use EncryptedSharedPreferences than default implementation of SharedPreferences. Since EncryptedSharedPreferences still implements SharedPreferences interface, you can rewrite the Android part of your code in the following way:
actual fun getSettings(context: AppContext?): Settings { | |
requireNotNull(context) | |
val masterKey = MasterKey.Builder(context) | |
.setKeyScheme(KeyScheme.AES256_GCM) | |
.build() | |
val encryptedPreferences = EncryptedSharedPreferences.create( | |
context, | |
context.packageName + "_encrypted_preferences", | |
masterKey, | |
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, | |
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM | |
) | |
return SharedPreferencesSettings(encryptedPreferences) | |
} |
In iOS secure data can be stored in the keychain
@OptIn(ExperimentalSettingsImplementation::class) | |
actual fun getEncryptedSettings(context: AppContext?): Settings = | |
KeychainSettings("${NSBundle.mainBundle.bundleIdentifier}.AUTH") |
At the end, I’d also like to share my experience with you in migration from default key-value storage to more secure implementation in each platform. I had some values, which had to be stored in the secure way. Since the library doesn’t provide any migration mechanism out of the box, here’s the way how I solved this issue — I modified the way of retrieving encrypted preferences (Keychain) in the following way:
In common sourceSet nothing was changed
In android sourceSet now code looks like this:
actual fun getNonEncryptedSettings(context: AppContext?): Settings = SharedPreferencesSettings( | |
PreferenceManager.getDefaultSharedPreferences(context) | |
) | |
actual fun getEncryptedSettings(context: AppContext?): Settings { | |
requireNotNull(context) | |
val masterKey = MasterKey.Builder(context) | |
.setKeyScheme(KeyScheme.AES256_GCM) | |
.build() | |
val encryptedPreferences = EncryptedSharedPreferences.create( | |
context, | |
context.packageName + "_encrypted_preferences", | |
masterKey, | |
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, | |
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM | |
) | |
val oldPreferences = getNonEncryptedSettings(context) | |
return SharedPreferencesSettings(MigrationPreferences(listOf("key1", "key2"), oldPreferences, encryptedPreferences)) | |
} | |
class MigrationPreferences( | |
keysToMigrate: List<String>, | |
oldPreferences: Settings, | |
private val encryptedPreferences: SharedPreferences | |
) : SharedPreferences by encryptedPreferences { | |
init { | |
encryptedPreferences.edit { | |
keysToMigrate.forEach { key -> | |
val value = oldPreferences.getStringOrNull(key) | |
value?.let { | |
putString(key, it) | |
oldPreferences.remove(key) | |
} | |
} | |
} | |
} | |
} |
and here’s the iOS part:
actual fun getNonEncryptedSettings(context: AppContext?): Settings = | |
NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults) | |
@OptIn(ExperimentalSettingsImplementation::class) | |
actual fun getEncryptedSettings(context: AppContext?): Settings = | |
MigrationSettings( | |
listOf("key1", "key2"), | |
getNonEncryptedSettings(context), | |
KeychainSettings("${NSBundle.mainBundle.bundleIdentifier}.AUTH") | |
) | |
@OptIn(ExperimentalSettingsImplementation::class) | |
private class MigrationSettings( | |
keysToMigrate: List<String>, | |
oldSettings: Settings, | |
val keychainSettings: KeychainSettings | |
) : Settings by keychainSettings { | |
init { | |
keysToMigrate.forEach { key -> | |
val value = oldSettings.getStringOrNull(key) | |
value?.let { | |
putString(key, it) | |
oldSettings.remove(key) | |
} | |
} | |
} | |
} |
Hope, that this article would help you!
Thank you for reading! Feel free to ask questions and leave the feedback in comments or Linkedin.
This article was previously published on proandroiddev.com