Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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,9 @@ 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 app.futured.androidprojecttemplate.tools.extensions.dataStore
import app.futured.androidprojecttemplate.tools.serialization.ZonedDateTimeSerializer
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -33,8 +35,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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package app.futured.androidprojecttemplate.tools.extensions

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import app.futured.androidprojecttemplate.tools.Constants

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = Constants.DataStore.DEFAULT_DATASTORE_NAME)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extension is only used to create a datastore in Hilt module, and could be potentially misused by user to create a new datastore instead of injecting it. I would remove this and implement everything inside ApplicationModule, or at least move it to the ApplicationModule file and make it private.

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