Merge pull request #29 from kirmanak/data-store
Migrate from Shared Preferences to Data Store
This commit is contained in:
@@ -120,9 +120,6 @@ dependencies {
|
||||
// https://github.com/Kotlin/kotlinx.serialization/releases
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/preference
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
|
||||
// https://github.com/JakeWharton/timber/releases
|
||||
implementation 'com.jakewharton.timber:timber:5.0.1'
|
||||
|
||||
@@ -145,6 +142,12 @@ dependencies {
|
||||
// https://github.com/square/picasso/releases
|
||||
implementation "com.squareup.picasso:picasso:2.8"
|
||||
|
||||
// https://github.com/androidbroadcast/ViewBindingPropertyDelegate/releases
|
||||
implementation "com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6"
|
||||
|
||||
// https://developer.android.com/topic/libraries/architecture/datastore
|
||||
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||
|
||||
// https://github.com/junit-team/junit4/releases
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
|
||||
@@ -163,9 +166,6 @@ dependencies {
|
||||
// https://mockk.io/
|
||||
testImplementation "io.mockk:mockk:1.12.3"
|
||||
|
||||
// https://github.com/androidbroadcast/ViewBindingPropertyDelegate/releases
|
||||
implementation "com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6"
|
||||
|
||||
// https://github.com/facebook/flipper/releases
|
||||
def flipper_version = "0.140.0"
|
||||
debugImplementation "com.facebook.flipper:flipper:$flipper_version"
|
||||
|
||||
@@ -25,54 +25,54 @@ import javax.inject.Singleton
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DebugModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun provideLoggingInterceptor(): Interceptor {
|
||||
val interceptor = HttpLoggingInterceptor { message -> Timber.tag("OkHttp").v(message) }
|
||||
interceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||
return interceptor
|
||||
}
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun provideLoggingInterceptor(): Interceptor {
|
||||
val interceptor = HttpLoggingInterceptor { message -> Timber.tag("OkHttp").v(message) }
|
||||
interceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||
return interceptor
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun provideFlipperInterceptor(networkFlipperPlugin: NetworkFlipperPlugin): Interceptor {
|
||||
return FlipperOkhttpInterceptor(networkFlipperPlugin)
|
||||
}
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun provideFlipperInterceptor(networkFlipperPlugin: NetworkFlipperPlugin): Interceptor {
|
||||
return FlipperOkhttpInterceptor(networkFlipperPlugin)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun networkFlipperPlugin() = NetworkFlipperPlugin()
|
||||
@Provides
|
||||
@Singleton
|
||||
fun networkFlipperPlugin() = NetworkFlipperPlugin()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun bindNetworkFlipperPlugin(plugin: NetworkFlipperPlugin): FlipperPlugin = plugin
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun bindNetworkFlipperPlugin(plugin: NetworkFlipperPlugin): FlipperPlugin = plugin
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun sharedPreferencesPlugin(@ApplicationContext context: Context): FlipperPlugin =
|
||||
SharedPreferencesFlipperPlugin(context)
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun sharedPreferencesPlugin(@ApplicationContext context: Context): FlipperPlugin =
|
||||
SharedPreferencesFlipperPlugin(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun leakCanaryPlugin(): FlipperPlugin {
|
||||
LeakCanary.config = LeakCanary.config.copy(onHeapAnalyzedListener = FlipperLeakListener())
|
||||
return LeakCanary2FlipperPlugin()
|
||||
}
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun leakCanaryPlugin(): FlipperPlugin {
|
||||
LeakCanary.config = LeakCanary.config.copy(onHeapAnalyzedListener = FlipperLeakListener())
|
||||
return LeakCanary2FlipperPlugin()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun databasesPlugin(@ApplicationContext context: Context): FlipperPlugin =
|
||||
DatabasesFlipperPlugin(context)
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun databasesPlugin(@ApplicationContext context: Context): FlipperPlugin =
|
||||
DatabasesFlipperPlugin(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun inspectorPlugin(@ApplicationContext context: Context): FlipperPlugin =
|
||||
InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun inspectorPlugin(@ApplicationContext context: Context): FlipperPlugin =
|
||||
InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package gq.kirmanak.mealient.data
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import gq.kirmanak.mealient.data.impl.util.RoomTypeConverters
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeDao
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.*
|
||||
import gq.kirmanak.mealient.extensions.RoomTypeConverters
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
|
||||
@@ -15,5 +15,5 @@ interface AuthRepo {
|
||||
|
||||
fun authenticationStatuses(): Flow<Boolean>
|
||||
|
||||
fun logout()
|
||||
suspend fun logout()
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package gq.kirmanak.mealient.data.auth
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AuthStorage {
|
||||
fun storeAuthData(authHeader: String, baseUrl: String)
|
||||
suspend fun storeAuthData(authHeader: String, baseUrl: String)
|
||||
|
||||
suspend fun getBaseUrl(): String?
|
||||
|
||||
@@ -11,5 +11,5 @@ interface AuthStorage {
|
||||
|
||||
fun authHeaderObservable(): Flow<String?>
|
||||
|
||||
fun clearAuthData()
|
||||
suspend fun clearAuthData()
|
||||
}
|
||||
@@ -2,9 +2,9 @@ package gq.kirmanak.mealient.data.auth.impl
|
||||
|
||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthenticationError.*
|
||||
import gq.kirmanak.mealient.data.impl.ErrorDetail
|
||||
import gq.kirmanak.mealient.data.impl.util.decodeErrorBodyOrNull
|
||||
import gq.kirmanak.mealient.data.network.ErrorDetail
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -12,7 +12,9 @@ import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AuthDataSourceImpl @Inject constructor(
|
||||
private val authServiceFactory: ServiceFactory<AuthService>,
|
||||
private val json: Json,
|
||||
|
||||
@@ -11,7 +11,9 @@ import kotlinx.coroutines.flow.map
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AuthRepoImpl @Inject constructor(
|
||||
private val dataSource: AuthDataSource,
|
||||
private val storage: AuthStorage,
|
||||
@@ -44,7 +46,7 @@ class AuthRepoImpl @Inject constructor(
|
||||
return storage.authHeaderObservable().map { it != null }
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
override suspend fun logout() {
|
||||
Timber.v("logout() called")
|
||||
storage.clearAuthData()
|
||||
}
|
||||
|
||||
@@ -1,53 +1,48 @@
|
||||
package gq.kirmanak.mealient.data.auth.impl
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||
import gq.kirmanak.mealient.data.impl.util.changesFlow
|
||||
import gq.kirmanak.mealient.data.impl.util.getStringOrNull
|
||||
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val AUTH_HEADER_KEY = "AUTH_TOKEN"
|
||||
private const val BASE_URL_KEY = "BASE_URL"
|
||||
|
||||
@Singleton
|
||||
class AuthStorageImpl @Inject constructor(
|
||||
private val sharedPreferences: SharedPreferences
|
||||
private val preferencesStorage: PreferencesStorage,
|
||||
) : AuthStorage {
|
||||
|
||||
override fun storeAuthData(authHeader: String, baseUrl: String) {
|
||||
private val authHeaderKey by preferencesStorage::authHeaderKey
|
||||
private val baseUrlKey by preferencesStorage::baseUrlKey
|
||||
|
||||
override suspend fun storeAuthData(authHeader: String, baseUrl: String) {
|
||||
Timber.v("storeAuthData() called with: authHeader = $authHeader, baseUrl = $baseUrl")
|
||||
sharedPreferences.edit {
|
||||
putString(AUTH_HEADER_KEY, authHeader)
|
||||
putString(BASE_URL_KEY, baseUrl)
|
||||
}
|
||||
preferencesStorage.storeValues(
|
||||
Pair(authHeaderKey, authHeader),
|
||||
Pair(baseUrlKey, baseUrl),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getBaseUrl(): String? {
|
||||
val baseUrl = sharedPreferences.getStringOrNull(BASE_URL_KEY)
|
||||
val baseUrl = preferencesStorage.getValue(baseUrlKey)
|
||||
Timber.d("getBaseUrl: base url is $baseUrl")
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
override suspend fun getAuthHeader(): String? {
|
||||
Timber.v("getAuthHeader() called")
|
||||
val token = sharedPreferences.getStringOrNull(AUTH_HEADER_KEY)
|
||||
val token = preferencesStorage.getValue(authHeaderKey)
|
||||
Timber.d("getAuthHeader: header is \"$token\"")
|
||||
return token
|
||||
}
|
||||
|
||||
override fun authHeaderObservable(): Flow<String?> {
|
||||
Timber.v("authHeaderObservable() called")
|
||||
return sharedPreferences.changesFlow().map { it.first.getStringOrNull(AUTH_HEADER_KEY) }
|
||||
return preferencesStorage.valueUpdates(authHeaderKey)
|
||||
}
|
||||
|
||||
override fun clearAuthData() {
|
||||
override suspend fun clearAuthData() {
|
||||
Timber.v("clearAuthData() called")
|
||||
sharedPreferences.edit {
|
||||
remove(AUTH_HEADER_KEY)
|
||||
remove(BASE_URL_KEY)
|
||||
}
|
||||
preferencesStorage.removeValues(authHeaderKey, baseUrlKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ package gq.kirmanak.mealient.data.disclaimer
|
||||
interface DisclaimerStorage {
|
||||
suspend fun isDisclaimerAccepted(): Boolean
|
||||
|
||||
fun acceptDisclaimer()
|
||||
suspend fun acceptDisclaimer()
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
package gq.kirmanak.mealient.data.disclaimer
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import gq.kirmanak.mealient.data.impl.util.getBooleanOrFalse
|
||||
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val IS_DISCLAIMER_ACCEPTED_KEY = "IS_DISCLAIMER_ACCEPTED"
|
||||
|
||||
@Singleton
|
||||
class DisclaimerStorageImpl @Inject constructor(
|
||||
private val sharedPreferences: SharedPreferences
|
||||
private val preferencesStorage: PreferencesStorage,
|
||||
) : DisclaimerStorage {
|
||||
|
||||
private val isDisclaimerAcceptedKey by preferencesStorage::isDisclaimerAcceptedKey
|
||||
|
||||
override suspend fun isDisclaimerAccepted(): Boolean {
|
||||
Timber.v("isDisclaimerAccepted() called")
|
||||
val isAccepted = sharedPreferences.getBooleanOrFalse(IS_DISCLAIMER_ACCEPTED_KEY)
|
||||
val isAccepted = preferencesStorage.getValue(isDisclaimerAcceptedKey) ?: false
|
||||
Timber.v("isDisclaimerAccepted() returned: $isAccepted")
|
||||
return isAccepted
|
||||
}
|
||||
|
||||
override fun acceptDisclaimer() {
|
||||
override suspend fun acceptDisclaimer() {
|
||||
Timber.v("acceptDisclaimer() called")
|
||||
sharedPreferences.edit()
|
||||
.putBoolean(IS_DISCLAIMER_ACCEPTED_KEY, true)
|
||||
.apply()
|
||||
preferencesStorage.storeValues(Pair(isDisclaimerAcceptedKey, true))
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.impl.util
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.onClosed
|
||||
import kotlinx.coroutines.channels.onFailure
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
suspend fun SharedPreferences.getStringOrNull(key: String) =
|
||||
withContext(Dispatchers.IO) { getString(key, null) }
|
||||
|
||||
suspend fun SharedPreferences.getBooleanOrFalse(key: String) =
|
||||
withContext(Dispatchers.IO) { getBoolean(key, false) }
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun SharedPreferences.changesFlow(): Flow<Pair<SharedPreferences, String?>> = callbackFlow {
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { prefs, key ->
|
||||
Timber.v("watchChanges: listener called with key $key")
|
||||
trySend(prefs to key)
|
||||
.onFailure { Timber.e(it, "watchChanges: can't send preference change, key $key") }
|
||||
.onClosed { Timber.e(it, "watchChanges: flow has been closed") }
|
||||
}
|
||||
Timber.v("watchChanges: registering listener")
|
||||
registerOnSharedPreferenceChangeListener(listener)
|
||||
send(this@changesFlow to null)
|
||||
awaitClose {
|
||||
Timber.v("watchChanges: flow has been closed")
|
||||
unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package gq.kirmanak.mealient.data.impl
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -1,4 +1,4 @@
|
||||
package gq.kirmanak.mealient.data.impl
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -1,4 +1,4 @@
|
||||
package gq.kirmanak.mealient.data.impl
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
@@ -8,7 +8,9 @@ import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RetrofitBuilder @Inject constructor(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val json: Json
|
||||
@@ -1,6 +1,5 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import gq.kirmanak.mealient.data.impl.RetrofitBuilder
|
||||
import timber.log.Timber
|
||||
|
||||
inline fun <reified T> RetrofitBuilder.createServiceFactory() =
|
||||
|
||||
@@ -3,17 +3,19 @@ package gq.kirmanak.mealient.data.recipes.db
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.withTransaction
|
||||
import gq.kirmanak.mealient.data.AppDb
|
||||
import gq.kirmanak.mealient.data.impl.util.recipeEntity
|
||||
import gq.kirmanak.mealient.data.impl.util.toRecipeEntity
|
||||
import gq.kirmanak.mealient.data.impl.util.toRecipeIngredientEntity
|
||||
import gq.kirmanak.mealient.data.impl.util.toRecipeInstructionEntity
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.*
|
||||
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.extensions.recipeEntity
|
||||
import gq.kirmanak.mealient.extensions.toRecipeEntity
|
||||
import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity
|
||||
import gq.kirmanak.mealient.extensions.toRecipeInstructionEntity
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecipeStorageImpl @Inject constructor(
|
||||
private val db: AppDb
|
||||
) : RecipeStorage {
|
||||
|
||||
@@ -9,7 +9,9 @@ import gq.kirmanak.mealient.ui.ImageLoader
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecipeImageLoaderImpl @Inject constructor(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val authRepo: AuthRepo
|
||||
|
||||
@@ -11,8 +11,10 @@ import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
@Singleton
|
||||
class RecipeRepoImpl @Inject constructor(
|
||||
private val mediator: RecipesRemoteMediator,
|
||||
private val storage: RecipeStorage,
|
||||
|
||||
@@ -10,8 +10,10 @@ import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
@Singleton
|
||||
class RecipesRemoteMediator @Inject constructor(
|
||||
private val storage: RecipeStorage,
|
||||
private val network: RecipeDataSource,
|
||||
|
||||
@@ -6,7 +6,9 @@ import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecipeDataSourceImpl @Inject constructor(
|
||||
private val authRepo: AuthRepo,
|
||||
private val recipeServiceFactory: ServiceFactory<RecipeService>,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package gq.kirmanak.mealient.data.storage
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PreferencesStorage {
|
||||
|
||||
val baseUrlKey: Preferences.Key<String>
|
||||
|
||||
val authHeaderKey: Preferences.Key<String>
|
||||
|
||||
val isDisclaimerAcceptedKey: Preferences.Key<Boolean>
|
||||
|
||||
suspend fun <T> getValue(key: Preferences.Key<T>): T?
|
||||
|
||||
suspend fun <T> requireValue(key: Preferences.Key<T>): T
|
||||
|
||||
suspend fun <T> storeValues(vararg pairs: Pair<Preferences.Key<T>, T>)
|
||||
|
||||
fun <T> valueUpdates(key: Preferences.Key<T>): Flow<T?>
|
||||
|
||||
suspend fun <T> removeValues(vararg keys: Preferences.Key<T>)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package gq.kirmanak.mealient.data.storage
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class PreferencesStorageImpl @Inject constructor(
|
||||
private val dataStore: DataStore<Preferences>
|
||||
) : PreferencesStorage {
|
||||
|
||||
override val baseUrlKey = stringPreferencesKey("baseUrl")
|
||||
|
||||
override val authHeaderKey = stringPreferencesKey("authHeader")
|
||||
|
||||
override val isDisclaimerAcceptedKey = booleanPreferencesKey("isDisclaimedAccepted")
|
||||
|
||||
override suspend fun <T> getValue(key: Preferences.Key<T>): T? {
|
||||
val value = dataStore.data.first()[key]
|
||||
Timber.v("getValue() returned: $value for $key")
|
||||
return value
|
||||
}
|
||||
|
||||
override suspend fun <T> requireValue(key: Preferences.Key<T>): T =
|
||||
checkNotNull(getValue(key)) { "Value at $key is null when it was required" }
|
||||
|
||||
override suspend fun <T> storeValues(vararg pairs: Pair<Preferences.Key<T>, T>) {
|
||||
Timber.v("storeValues() called with: pairs = ${pairs.contentToString()}")
|
||||
dataStore.edit { preferences ->
|
||||
pairs.forEach { preferences += it.toPreferencesPair() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> valueUpdates(key: Preferences.Key<T>): Flow<T?> {
|
||||
Timber.v("valueUpdates() called with: key = $key")
|
||||
return dataStore.data
|
||||
.map { it[key] }
|
||||
.distinctUntilChanged()
|
||||
.onEach { Timber.d("valueUpdates: new value at $key is $it") }
|
||||
.onCompletion { Timber.i(it, "valueUpdates: finished") }
|
||||
}
|
||||
|
||||
override suspend fun <T> removeValues(vararg keys: Preferences.Key<T>) {
|
||||
Timber.v("removeValues() called with: key = ${keys.contentToString()}")
|
||||
dataStore.edit { preferences ->
|
||||
keys.forEach { preferences -= it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Pair<Preferences.Key<T>, T>.toPreferencesPair(): Preferences.Pair<T> {
|
||||
val (key, value) = this
|
||||
return key to value
|
||||
}
|
||||
@@ -1,42 +1,39 @@
|
||||
package gq.kirmanak.mealient.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import androidx.room.Room
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.data.AppDb
|
||||
import gq.kirmanak.mealient.data.impl.OkHttpBuilder
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||
import gq.kirmanak.mealient.data.storage.PreferencesStorageImpl
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun createDb(@ApplicationContext context: Context): AppDb =
|
||||
Room.databaseBuilder(context, AppDb::class.java, "app.db").build()
|
||||
interface AppModule {
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun createSharedPreferences(@ApplicationContext context: Context): SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun createDb(@ApplicationContext context: Context): AppDb =
|
||||
Room.databaseBuilder(context, AppDb::class.java, "app.db").build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun createOkHttp(okHttpBuilder: OkHttpBuilder): OkHttpClient =
|
||||
okHttpBuilder.buildOkHttp()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun createJson(): Json = Json {
|
||||
coerceInputValues = true
|
||||
ignoreUnknownKeys = true
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
|
||||
PreferenceDataStoreFactory.create { context.preferencesDataStoreFile("settings") }
|
||||
}
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindPreferencesStorage(preferencesStorage: PreferencesStorageImpl): PreferencesStorage
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
package gq.kirmanak.mealient.di
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
@@ -15,7 +12,7 @@ import gq.kirmanak.mealient.data.auth.impl.AuthDataSourceImpl
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthRepoImpl
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthService
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl
|
||||
import gq.kirmanak.mealient.data.impl.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.data.network.createServiceFactory
|
||||
import javax.inject.Singleton
|
||||
|
||||
27
app/src/main/java/gq/kirmanak/mealient/di/NetworkModule.kt
Normal file
27
app/src/main/java/gq/kirmanak/mealient/di/NetworkModule.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package gq.kirmanak.mealient.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.data.network.OkHttpBuilder
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun createOkHttp(okHttpBuilder: OkHttpBuilder): OkHttpClient =
|
||||
okHttpBuilder.buildOkHttp()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun createJson(): Json = Json {
|
||||
coerceInputValues = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.data.impl.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.data.network.createServiceFactory
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package gq.kirmanak.mealient.data.impl.util
|
||||
package gq.kirmanak.mealient.extensions
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -1,4 +1,4 @@
|
||||
package gq.kirmanak.mealient.data.impl.util
|
||||
package gq.kirmanak.mealient.extensions
|
||||
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeEntity
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeIngredientEntity
|
||||
@@ -1,4 +1,4 @@
|
||||
package gq.kirmanak.mealient.data.impl.util
|
||||
package gq.kirmanak.mealient.extensions
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import kotlinx.datetime.*
|
||||
@@ -38,7 +38,9 @@ class AuthenticationViewModel @Inject constructor(
|
||||
|
||||
fun logout() {
|
||||
Timber.v("logout() called")
|
||||
authRepo.logout()
|
||||
viewModelScope.launch { recipeRepo.clearLocalData() }
|
||||
viewModelScope.launch {
|
||||
authRepo.logout()
|
||||
recipeRepo.clearLocalData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,10 @@ class DisclaimerViewModel @Inject constructor(
|
||||
|
||||
fun acceptDisclaimer() {
|
||||
Timber.v("acceptDisclaimer() called")
|
||||
disclaimerStorage.acceptDisclaimer()
|
||||
_isAccepted.value = true
|
||||
viewModelScope.launch {
|
||||
disclaimerStorage.acceptDisclaimer()
|
||||
_isAccepted.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun startCountDown() {
|
||||
|
||||
@@ -5,7 +5,9 @@ import com.squareup.picasso.Picasso
|
||||
import gq.kirmanak.mealient.ui.ImageLoader
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ImageLoaderPicasso @Inject constructor(
|
||||
private val picasso: Picasso
|
||||
) : ImageLoader {
|
||||
|
||||
@@ -8,7 +8,9 @@ import gq.kirmanak.mealient.BuildConfig
|
||||
import okhttp3.OkHttpClient
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class PicassoBuilder @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val okHttpClient: OkHttpClient
|
||||
|
||||
@@ -3,7 +3,7 @@ package gq.kirmanak.mealient.data.auth.impl
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthenticationError.*
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.di.AppModule
|
||||
import gq.kirmanak.mealient.di.NetworkModule
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
|
||||
@@ -33,7 +33,7 @@ class AuthDataSourceImplTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
subject = AuthDataSourceImpl(authServiceFactory, AppModule.createJson())
|
||||
subject = AuthDataSourceImpl(authServiceFactory, NetworkModule.createJson())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -13,8 +13,8 @@ import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME
|
||||
import gq.kirmanak.mealient.test.RobolectricTest
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
@@ -102,12 +102,12 @@ class AuthRepoImplTest : RobolectricTest() {
|
||||
dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD), eq(TEST_BASE_URL))
|
||||
} returns TEST_TOKEN
|
||||
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, TEST_BASE_URL)
|
||||
verify { storage.storeAuthData(TEST_AUTH_HEADER, TEST_BASE_URL) }
|
||||
coVerify { storage.storeAuthData(TEST_AUTH_HEADER, TEST_BASE_URL) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when logout then clearAuthData is called`() = runTest {
|
||||
subject.logout()
|
||||
verify { storage.clearAuthData() }
|
||||
coVerify { storage.clearAuthData() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package gq.kirmanak.mealient.data.storage
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltAndroidTest
|
||||
class PreferencesStorageImplTest : HiltRobolectricTest() {
|
||||
|
||||
@Inject
|
||||
lateinit var subject: PreferencesStorage
|
||||
|
||||
@Test
|
||||
fun `when getValue without writes then null`() = runTest {
|
||||
assertThat(subject.getValue(subject.authHeaderKey)).isNull()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun `when requireValue without writes then throws IllegalStateException`() = runTest {
|
||||
subject.requireValue(subject.authHeaderKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when getValue after write then returns value`() = runTest {
|
||||
subject.storeValues(Pair(subject.authHeaderKey, "test"))
|
||||
assertThat(subject.getValue(subject.authHeaderKey)).isEqualTo("test")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when storeValue then valueUpdates emits`() = runTest {
|
||||
subject.storeValues(Pair(subject.authHeaderKey, "test"))
|
||||
assertThat(subject.valueUpdates(subject.authHeaderKey).first()).isEqualTo("test")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when remove value then getValue returns null`() = runTest {
|
||||
subject.storeValues(Pair(subject.authHeaderKey, "test"))
|
||||
subject.removeValues(subject.authHeaderKey)
|
||||
assertThat(subject.getValue(subject.authHeaderKey)).isNull()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package gq.kirmanak.mealient.data.impl
|
||||
package gq.kirmanak.mealient.extensions
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import gq.kirmanak.mealient.data.impl.util.RoomTypeConverters
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import org.junit.Test
|
||||
@@ -8,22 +8,16 @@ import org.junit.BeforeClass
|
||||
import org.junit.Rule
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
import timber.log.Timber
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(application = HiltTestApplication::class, manifest = Config.NONE)
|
||||
abstract class HiltRobolectricTest {
|
||||
|
||||
companion object {
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun setupTimber() {
|
||||
Timber.plant(object : Timber.Tree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
println(message)
|
||||
t?.printStackTrace()
|
||||
}
|
||||
})
|
||||
}
|
||||
fun setupTimber() = plantPrintLn()
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
|
||||
@@ -2,9 +2,18 @@ package gq.kirmanak.mealient.test
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(application = Application::class, manifest = Config.NONE)
|
||||
abstract class RobolectricTest
|
||||
abstract class RobolectricTest {
|
||||
|
||||
companion object {
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun setupTimber() = plantPrintLn()
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,15 @@ package gq.kirmanak.mealient.test
|
||||
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import timber.log.Timber
|
||||
|
||||
fun String.toJsonResponseBody() = toResponseBody("application/json".toMediaType())
|
||||
fun String.toJsonResponseBody() = toResponseBody("application/json".toMediaType())
|
||||
|
||||
fun plantPrintLn() {
|
||||
Timber.plant(object : Timber.Tree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
println(message)
|
||||
t?.printStackTrace()
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user