diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 232e7d6..68edb1c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -156,8 +156,9 @@ dependencies { implementation(Dependencies.Support.lifecycleCompiler) coreLibraryDesugaring(Dependencies.Support.desugarLibs) + implementation(Dependencies.Support.datastore) + implementation(Dependencies.Support.vectordrawable) - implementation(Dependencies.Support.preference) // Compose implementation(Dependencies.Compose.animation) diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/JsonPersistence.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/JsonPersistence.kt new file mode 100644 index 0000000..0b78fb0 --- /dev/null +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/JsonPersistence.kt @@ -0,0 +1,104 @@ +package app.futured.androidprojecttemplate.data.persistence + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * [DataStore]-backed Persistence which allows storage and observing of complex JSON objects. + * Uses [kotlinx.serialization] to serialize and deserialize objects into Strings. + */ +@Singleton +class JsonPersistence @Inject constructor( + val dataStore: DataStore, + val json: Json, +) { + + /** + * Saves provided [value] in persistence under provided [key]. + */ + suspend inline fun save( + key: Preferences.Key, + value: T, + ) { + dataStore.edit { preferences -> preferences[key] = json.encodeToString(value) } + } + + /** + * Returns persisted object with provided [key]. + * + * @return Persisted object, or `null` if does not exist. + * @throws [SerializationException] if object could not be deserialized as [T]. + */ + suspend inline fun get(key: Preferences.Key): T? = + dataStore.data.firstOrNull() + ?.let { preferences -> preferences[key] } + ?.let { jsonString -> + json.decodeFromString(jsonString) + } + + /** + * Returns a [Flow] of persisted objects with provided [key]. + * + * @return [Flow] of objects. Values in [Flow] can be `null` if not found in persistence. + * @throws [SerializationException] if object could not be deserialized as [T]. + */ + inline fun observe(key: Preferences.Key): Flow = + dataStore.data + .map { preferences -> preferences[key] } + .map { jsonString -> + jsonString?.let { json.decodeFromString(jsonString) } + } + + /** + * Removes persisted object with provided [key]. + */ + suspend fun delete(key: Preferences.Key) = dataStore.edit { preferences -> preferences.remove(key) } + + /** + * Returns persisted object with provided [key], catching any [SerializationException]s and mapping them to `null`. + * + * @return Persisted object, or `null` if does not exist or object cannot be deserialized as [T]. + */ + suspend inline fun getCatching(key: Preferences.Key): T? = + runCatching { get(key) } + .recoverCatching { error -> + if (error is SerializationException) { + Timber.tag("JsonPersistence").e(error, "Unable to deserialize property with key '${key.name}'") + null + } else { + throw error + } + } + .getOrThrow() + + /** + * Returns a [Flow] of persisted objects with provided [key], catching any [SerializationException]s that might occur + * and mapping them to `null` values. + * + * @return [Flow] of objects. Values in [Flow] can be `null` if not found in persistence or cannot be deserialized as [T]. + */ + inline fun observeCatching(key: Preferences.Key): Flow = + dataStore.data + .map { preferences -> preferences[key] } + .map { jsonString -> + runCatching { jsonString?.let { json.decodeFromString(jsonString) } } + .recoverCatching { error -> + if (error is SerializationException) { + Timber.tag("JsonPersistence").e(error, "Unable to deserialize property with key '${key.name}'") + null + } else { + null + } + }.getOrThrow() + } +} diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/Persistence.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/Persistence.kt deleted file mode 100644 index bde4f53..0000000 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/Persistence.kt +++ /dev/null @@ -1,46 +0,0 @@ -package app.futured.androidprojecttemplate.data.persistence - -import android.content.SharedPreferences -import androidx.core.content.edit -import kotlinx.serialization.json.Json -import kotlinx.serialization.serializer -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.reflect.KClass - -@Singleton -class Persistence @Inject constructor( - val json: Json, - val sharedPreferences: SharedPreferences, -) { - - inline operator fun set(key: String, value: T) = - sharedPreferences.edit { putString(key, json.encodeToString(serializer(), value)) } - - inline operator fun get(key: String): T = - get(T::class, key, null)!! - - inline operator fun get(key: String, defaultValue: T): T = - get(T::class, key, defaultValue)!! - - inline fun getOrNull(key: String): T? = - get(T::class, key, null) - - fun delete(key: String) = sharedPreferences.edit { - this.remove(key) - } - - operator fun contains(key: String): Boolean = sharedPreferences.contains(key) - - fun clear() = sharedPreferences.edit { clear() } - - @Suppress("UNUSED_PARAMETER") - inline fun get(clazz: KClass, key: String, defaultValue: T? = null): T? { - val persistedValue = sharedPreferences.getString(key, null) - return if (persistedValue == null) { - defaultValue - } else { - json.decodeFromString(serializer(), persistedValue) - } - } -} diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/PrimitivePersistence.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/PrimitivePersistence.kt new file mode 100644 index 0000000..b6822eb --- /dev/null +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/data/persistence/PrimitivePersistence.kt @@ -0,0 +1,35 @@ +package app.futured.androidprojecttemplate.data.persistence + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull +import javax.inject.Inject +import javax.inject.Singleton + +/** + * [DataStore]-backed Persistence which allows storage and observing of persisted entities. + */ +@Singleton +class PrimitivePersistence @Inject constructor( + private val dataStore: DataStore, +) { + + suspend fun get(key: Preferences.Key): T? = dataStore.data.firstOrNull()?.run { + this[key] + } + + fun observe(key: Preferences.Key): Flow = dataStore.data.mapNotNull { + it[key] + } + + suspend fun save(key: Preferences.Key, value: T) { + dataStore.edit { it[key] = value } + } + + suspend fun delete(key: Preferences.Key<*>) = dataStore.edit { + it.remove(key) + } +} diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/ApplicationModule.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/ApplicationModule.kt index 7cb13e7..982d3fd 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/ApplicationModule.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/injection/modules/ApplicationModule.kt @@ -2,7 +2,10 @@ package app.futured.androidprojecttemplate.injection.modules import android.content.Context import android.content.res.Resources -import androidx.preference.PreferenceManager +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import app.futured.androidprojecttemplate.tools.Constants import app.futured.androidprojecttemplate.tools.serialization.ZonedDateTimeSerializer import dagger.Module import dagger.Provides @@ -16,6 +19,9 @@ import kotlinx.serialization.modules.SerializersModule @Module @InstallIn(SingletonComponent::class) class ApplicationModule { + private val Context.dataStore: DataStore by preferencesDataStore( + name = Constants.DataStore.DEFAULT_DATASTORE_NAME + ) @Provides fun resources( @@ -33,8 +39,5 @@ class ApplicationModule { } @Provides - fun sharedPrefs( - @ApplicationContext context: Context, - ) = - PreferenceManager.getDefaultSharedPreferences(context) + fun dataStore(@ApplicationContext context: Context): DataStore = context.dataStore } diff --git a/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/Constants.kt b/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/Constants.kt index 27203d2..3a4aca0 100644 --- a/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/Constants.kt +++ b/app/src/main/kotlin/app/futured/androidprojecttemplate/tools/Constants.kt @@ -6,4 +6,8 @@ interface Constants { const val BASE_PROD_URL = "https://reqres.in/" const val TIMEOUT_IN_SECONDS = 30L } + + object DataStore { + const val DEFAULT_DATASTORE_NAME = "preferences" + } } diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 12fc120..7baed56 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -37,9 +37,10 @@ object Dependencies { const val lifecycleCompiler = "androidx.lifecycle:lifecycle-compiler:${Versions.lifecycle}" const val desugarLibs = "com.android.tools:desugar_jdk_libs:${Versions.desugarLibs}" + const val datastore = "androidx.datastore:datastore-preferences:${Versions.datastore}" + // Questionable const val vectordrawable = "androidx.vectordrawable:vectordrawable:${Versions.vectorDrawable}" - const val preference = "androidx.preference:preference-ktx:${Versions.preference}" } object NavigationComponents { diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index a0687d9..0bdad0b 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -27,7 +27,7 @@ object Versions { const val lifecycle = "2.7.0" const val constraintLayout = "1.0.1" const val vectorDrawable = "1.2.0-beta01" - const val preference = "1.2.1" + const val datastore = "1.1.2" const val activity = "1.9.0" const val desugarLibs = "2.0.4"