Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Preferences>,
val json: Json,
) {

/**
* Saves provided [value] in persistence under provided [key].
*/
suspend inline fun <reified T : Any> save(
key: Preferences.Key<String>,
value: T,
) {
dataStore.edit { preferences -> preferences[key] = json.encodeToString<T>(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 <reified T : Any> get(key: Preferences.Key<String>): T? =
dataStore.data.firstOrNull()
?.let { preferences -> preferences[key] }
?.let { jsonString ->
json.decodeFromString<T>(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 <reified T : Any> observe(key: Preferences.Key<String>): Flow<T?> =
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<String>) = 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 <reified T : Any> getCatching(key: Preferences.Key<String>): T? =
runCatching { get<T>(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 <reified T : Any> observeCatching(key: Preferences.Key<String>): Flow<T?> =
dataStore.data
.map { preferences -> preferences[key] }
.map { jsonString ->
runCatching { jsonString?.let { json.decodeFromString<T>(jsonString) } }
.recoverCatching { error ->
if (error is SerializationException) {
Timber.tag("JsonPersistence").e(error, "Unable to deserialize property with key '${key.name}'")
null
} else {
null
}
}.getOrThrow()
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<Preferences>,
) {

suspend fun <T : Any> get(key: Preferences.Key<T>): T? = dataStore.data.firstOrNull()?.run {
this[key]
}

fun <T : Any> observe(key: Preferences.Key<T>): Flow<T> = dataStore.data.mapNotNull {
it[key]
}

suspend fun <T : Any> save(key: Preferences.Key<T>, value: T) {
dataStore.edit { it[key] = value }
}

suspend fun delete(key: Preferences.Key<*>) = dataStore.edit {
it.remove(key)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,6 +19,9 @@ import kotlinx.serialization.modules.SerializersModule
@Module
@InstallIn(SingletonComponent::class)
class ApplicationModule {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = Constants.DataStore.DEFAULT_DATASTORE_NAME
)

@Provides
fun resources(
Expand All @@ -33,8 +39,5 @@ class ApplicationModule {
}

@Provides
fun sharedPrefs(
@ApplicationContext context: Context,
) =
PreferenceManager.getDefaultSharedPreferences(context)
fun dataStore(@ApplicationContext context: Context): DataStore<Preferences> = context.dataStore
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading