Blog Infos
Author
Published
Topics
Published
Topics
Introduction

First of all, let’s explain why I need the CookiesStorage. To identify the user, server sets JWT token into the cookies. And all requests from the mobile app should be sent with this cookie. When user logouts from the application, the token cookie is deleted since the session is finished.

Now let’s explain, how the previous CookiesStorage was implemented. Since the project has KMM modules (currently supported only Android part), for the networking I use the Ktor library. It allows us to install the HttpCookies plugin.

private val httpClient = HttpClient {
install(HttpCookies) {
storage = cookiesStorage
}
}
view raw client_setup.kt hosted with ❤ by GitHub

What relates to CookiesStorage, it is the subclass of Ktor’s CookiesStorage (io.ktor.client.plugins.cookies.CookiesStorage). Here’s the way how I implemented it.

class CookiesStorage(private val localStorageRepository: PreferencesRepository) :
io.ktor.client.plugins.cookies.CookiesStorage {
override suspend fun addCookie(requestUrl: Url, cookie: Cookie) {
if (CookieType.values().any { it.value == cookie.name }) {
localStorageRepository.saveValue(cookie.name, cookie.value)
}
}
override fun close() {
}
override suspend fun get(requestUrl: Url) =
mutableListOf<Cookie>().apply {
CookieType.values().forEach { cookie ->
localStorageRepository.getValue<String>(cookie.value, null)?.let {
println("Loaded: ${cookie.value}=$it")
add(Cookie(cookie.value, it))
}
}
}
}

As you can see, the logic is pretty simple. When the server sets the cookie, system checks if this cookie is supported by the app (exists in CookieType), we save it in the local storage (in case of the Android value is saved into SharedPreferences).

actual class PreferencesRepository actual constructor(actual val di: DI) {
private val context: Context by di.instance()
val preferences by lazy {
context.getSharedPreferences("fitnestapp", Context.MODE_PRIVATE)
}
actual fun <T> saveValue(key: String, value: T?) {
println("Saved with Android: key=$key, value=$value")
val editor = preferences.edit()
when (value) {
is String -> editor.putString(key, value)
}
editor.apply()
}
actual inline fun <reified T> getValue(key: String, defaultValue: T?): T? {
return when (T::class) {
String::class -> {
preferences.getString(key, defaultValue as String?) as T?
}
else -> null
}
}
}

And here we face the main problem. Since SharedPreferences are supported only in Android, so, we have to use the expect-actual mechanism from KMM and write implementations in iOS and Android.

But recently Android team announced the KMM version of DataStore. For my case it is a silver bullet, since it allows me to:

  1. Migrate from the SharedPreferences to DataStore (its advantages you can read in this brilliant article)
  2. Have the same multi platform code instead of expect-actual classes

Currently the biggest minus is that the library is in dev stage, but when did it stop the developers :). So, now let me explain, how did I migrate to the DataStore.

Point one.

We need to add the dependencies — androidx.datastore:datastore-preferences-core:1.1.0-dev01 (into shared part) and androidx.datastore:datastore-preferences:1.1.0-alpha01 (since it contains the SharedPreferencesMigration — will discuss it later). Note, that the versions are different.

Point two.

I created a separate repository for the DataStore. Here’s it’s listing

class DataStoreRepository internal constructor(
private val dataStore: DataStore<Preferences>
) : com.fitnest.domain.repository.DataStoreRepository {
override suspend fun saveString(key: String, value: String) {
dataStore.edit { it[stringPreferencesKey(key)] = value }
}
override suspend fun getString(key: String) = dataStore.data.map {
it[stringPreferencesKey(key)]
}.firstOrNull()
}

What do we have here?

  • First of all — now the repository is located in common module, instead of the Android and iOS parts. The class DataStoreRepository implements DataStoreRepository from the domain layer to achieve the dependency inversion principle and make the class easy to test.
  • Second — instead of di container (which we need to get the SharedPreferences), now we pass the DataStore<Preferences> directly. So, conceptually, nothing is changed.
  • Third — get and save values methods became suspend, since DataStore editing (in case of the reading values) is suspend. What about reading the values — I decided not to return Flow, because in the current implementation I execute the method on each request, so, I don’t need the observing mechanism. It’s enough to use firstOrNull, but since it’s also suspend — getString method became suspend.

Getting the value from the PreferencesDataStore is very similar to working with shared preferences, but instead of methods getString, getInt and so on, now we use PreferencesKeys (full list of them you can find here).

And CookiesStorage almost didn’t change. The only difference that instead of PreferencesRepository now I pass the DataStoreRepository. But probably it is the same entities.

class CookiesStorage(private val localStorageRepository: DataStoreRepository) :
io.ktor.client.plugins.cookies.CookiesStorage {
override suspend fun addCookie(requestUrl: Url, cookie: Cookie) {
if (CookieType.values().any { it.value == cookie.name }) {
localStorageRepository.saveString(cookie.name, cookie.value)
}
}
override fun close() {}
override suspend fun get(requestUrl: Url) = mutableListOf<Cookie>().apply {
CookieType.values().forEach { cookie ->
localStorageRepository.getString(cookie.value)?.let {
println("Loaded: ${cookie.value}=$it")
add(Cookie(cookie.value, it))
}
}
}
}

Job Offers

Job Offers


    Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Mobile Engineer

    OLX Group
    Remote, Portugal, Spain, Romania, Poland
    • Full Time
    apply now

    Senior Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

,

How to disrupt the market with KMM

At DriveScore, we have one goal: to disrupt the car insurance market through technology. You might have a similarly ambitious goal, and one of the questions you’ll have to answer is whether the set of…
Watch Video

Jobs

Almost the last …

And here we face a small problem — how to migrate the data, which is already stored in the SharedPreferences to DataStore. Luckily, it is easy. When we create the instance of the DataStore, we can pass the list of the migrations (remember, that you should have only one instance of DataStore in the same process to avoid the accessing the file simultaneously and catching the exception).

First — this is how DataStore creation looks like:

internal fun getDataStore(path: String, migrations: List<DataMigration<Preferences>>) =
PreferenceDataStoreFactory.createWithPath(migrations = migrations) { path.toPath() }
internal const val dataStoreFileName = "fitnest.preferences_pb"
view raw getDataStore.kt hosted with ❤ by GitHub

Second — here’s the way how I create the migrations list:

actual fun createMigrations(di: DI): List<DataMigration<Preferences>> {
val context by di.instance<Context>()
return listOf(SharedPreferencesMigration(context, "fitnestapp"))
}

Inside of the SharedPreferencesMigration we need to pass the SharedPreferences object or context and preferences name — to allow the function to get the SharedPreferences object under the hood. Also we can specify the keys we want to migrate as the third param.

Under the hood, the migration function looks simple, we just read all values from SharedPreferences, then filter only the values we need and save them into Preferences.

private fun getMigrationFunction(): suspend (SharedPreferencesView, Preferences) -> Preferences =
{ sharedPrefs: SharedPreferencesView, currentData: Preferences ->
// prefs.getAll is already filtered to our key set, but we don't want to overwrite
// already existing keys.
val currentKeys = currentData.asMap().keys.map { it.name }
val filteredSharedPreferences =
sharedPrefs.getAll().filter { (key, _) -> key !in currentKeys }
val mutablePreferences = currentData.toMutablePreferences()
for ((key, value) in filteredSharedPreferences) {
when (value) {
is Boolean -> mutablePreferences[
booleanPreferencesKey(key)
] = value
is Float -> mutablePreferences[
floatPreferencesKey(key)
] = value
is Int -> mutablePreferences[
intPreferencesKey(key)
] = value
is Long -> mutablePreferences[
longPreferencesKey(key)
] = value
is String -> mutablePreferences[
stringPreferencesKey(key)
] = value
is Set<*> -> {
@Suppress("UNCHECKED_CAST")
mutablePreferences[
stringSetPreferencesKey(key)
] = value as Set<String>
}
}
}
mutablePreferences.toPreferences()
}
Conclusion.
  1. Appearance of this DataStore in KMM is one more point to look into the side of the KMM for the Android Developers, since it allows us to use more familiar classes and methods in the all platforms. But keep in mind that the library is in dev version.
  2. It allows us to use more common code instead of the expect/actual.
  3. With the help of the Flow, we can observe the changes of the storage.
  4. For more complex situations (than keeping key-value) you can use ProtoDataStore (links are here and here).
  5. DataStore is a more powerful alternative to the Settings library.
  6. Has the migrations mechanism.

You can find the source code at the Github.

Thank you for reading! Feel free to ask questions and leave the feedback in comments or Linkedin.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
I thought Kotlin Multiplatform Mobile was complicated to use before I decided to try…
READ MORE
blog
In my previous story, I’ve talked about why I believe we can strongly improve…
READ MORE
blog
In Android development, modularization of the application is a very common pattern that helps…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu