Skip to content

Instantly share code, notes, and snippets.

@DevSrSouza
Last active October 9, 2022 16:09
Show Gist options
  • Save DevSrSouza/f97d1093cb39c768b7d2f53a2cd0af35 to your computer and use it in GitHub Desktop.
Save DevSrSouza/f97d1093cb39c768b7d2f53a2cd0af35 to your computer and use it in GitHub Desktop.
PoC Kotlin Storage (Key/Value) API simple and extensible
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onStart
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import java.util.UUID
public interface Storage<T : Any> {
public suspend fun store(container: T?)
public suspend fun read(): T?
public fun listen(): Flow<T?>
}
public class StorageImpl<T : Any>(
private val storageUnit: StorageUnit,
private val serializeStrategy: StorageSerializeStrategy<T>,
private val keyStrategy: StorageKeyStrategy,
) : Storage<T> {
private val sharedFlow = MutableSharedFlow<T?>()
override suspend fun store(container: T?) {
storageUnit.store(
key = keyStrategy.get(),
value = container?.let { serializeStrategy.serialize(it) }
)
sharedFlow.emit(container)
}
override suspend fun read(): T? =
storageUnit.read(key = keyStrategy.get())?.let { serializeStrategy.deserialize(it) }
override fun listen(): Flow<T?> {
return sharedFlow.onStart { emit(read()) }
}
}
public interface StorageSerializeStrategy<T : Any> {
public fun serialize(container: T): String
public fun deserialize(value: String): T
}
/**
* StorageUnit is the component that will interact with the platform why of Storage, for exemplo in Android could be Preferences
*/
public interface StorageUnit {
public fun store(key: String, value: String?)
public fun read(key: String): String?
}
/**
* StorageKeyStrategy is a component that let you change the key based on Context without the service that
* is listen to the key know the correct why of searching the value. For exemplo if you have a screen secured
* by authetication and you want to store a value locally for that user, if he logouts you want to keep it configuration
* only for that account, so you can have a StorageKeyStrategy that uses the Account ID for storing and retriving the value
* without the ViewModel or any service handle it by hand.
*/
public interface StorageKeyStrategy {
public suspend fun get(): String
}
public class SingleKeyStrategy(
private val key: String,
) : StorageKeyStrategy {
override suspend fun get(): String = key
}
// POC
// actual fun PlatformSessionKeyStrategy(keyPrefix: String)
// class IosSessionKeyStrategy
// class AndroidSessionKeyStrategy
public class SessionKeyStrategy(
private val keyPrefix: String,
private val sessionService: SessionService,
) : StorageKeyStrategy {
override suspend fun get(): String {
val uuid = sessionService.current().awaitFirst()?.user?.id ?: UUID("0000-000-000-0000") // TODO multiplatform
return "$keyPrefix-$uuid"
}
}
public class KtxStorageSerializeStrategy<T : Any>(
private val json: Json,
private val serializer: KSerializer<T>,
) : StorageSerializeStrategy<T> {
override fun serialize(container: T): String {
return json.encodeToString(serializer, container)
}
override fun deserialize(value: String): T {
return json.decodeFromString(serializer, value)
}
}
public class StringStorageSerializeStrategy : StorageSerializeStrategy<String> {
override fun serialize(container: String): String = container
override fun deserialize(value: String): String = value
}
public fun Storage(
storageUnit: StorageUnit,
keyStrategy: StorageKeyStrategy,
): Storage<String> =
StorageImpl(
storageUnit = storageUnit,
serializeStrategy = StringStorageSerializeStrategy(),
keyStrategy = keyStrategy,
)
public fun Storage(
storageUnit: StorageUnit,
key: String,
): Storage<String> =
Storage(
storageUnit = storageUnit,
keyStrategy = SingleKeyStrategy(key)
)
@JvmName("Storage-inline")
public inline fun <reified T : Any> Storage(
storageUnit: StorageUnit,
keyStrategy: StorageKeyStrategy,
): Storage<T> =
StorageImpl(
storageUnit = storageUnit,
serializeStrategy = KtxStorageSerializeStrategy(
json = Json { }, // TODO: injectable?
serializer = serializer<T>(),
),
keyStrategy = keyStrategy,
)
@JvmName("Storage-inline")
public inline fun <reified T : Any> Storage(
storageUnit: StorageUnit,
key: String,
): Storage<T> =
Storage<T>(
storageUnit = storageUnit,
keyStrategy = SingleKeyStrategy(key)
)
@DevSrSouza
Copy link
Author

DevSrSouza commented Oct 9, 2022

Usage for example:

Simpler way:

val storage = Storage(
  storageUnit = AndroidSecuredStorageUnit(context),
  key = "user-displayname",
)

storage.store("New Display Name")

storage.listen().collect {
   displayname.text = it
}

With DI and Data Class:

@Serializable
data class AppConfiguration(
  val isDarkTheme: Boolean,
)

public class AppConfigurationStorage(
    storageUnit: StorageUnit,
    key: String
) : Storage<AppConfiguration> by Storage<AppConfiguration>(storageUnit, key)

// KODEIN
bind<AndroidSecuredStorageUnit>() with singleton {
   AndroidSecuredStorageUnit(context = instance<Application>())
}

bind<AppConfigurationStorage>() with singleton {
   AppConfigurationStorage(
      storageUnit = instance<AndroidSecuredStorageUnit>(),
      key = "myapplication-configuration"
   )
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment