Use intermediate representation for AddRecipe draft

This commit is contained in:
Kirill Kamakin
2022-08-04 21:19:15 +02:00
parent 8784912cdb
commit 057651c60f
14 changed files with 154 additions and 119 deletions

View File

@@ -120,9 +120,6 @@ dependencies {
implementation(libs.kirich1409.viewBinding) implementation(libs.kirich1409.viewBinding)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.datastore.datastore)
implementation(libs.androidx.security.crypto)
implementation(platform(libs.google.firebase.bom)) implementation(platform(libs.google.firebase.bom))
implementation(libs.google.firebase.analyticsKtx) implementation(libs.google.firebase.analyticsKtx)

View File

@@ -1,13 +0,0 @@
package gq.kirmanak.mealient.data.add
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import kotlinx.coroutines.flow.Flow
interface AddRecipeStorage {
val updates: Flow<AddRecipeRequest>
suspend fun save(addRecipeRequest: AddRecipeRequest)
suspend fun clear()
}

View File

@@ -2,10 +2,15 @@ package gq.kirmanak.mealient.data.add.impl
import gq.kirmanak.mealient.data.add.AddRecipeDataSource import gq.kirmanak.mealient.data.add.AddRecipeDataSource
import gq.kirmanak.mealient.data.add.AddRecipeRepo import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.data.add.AddRecipeStorage import gq.kirmanak.mealient.data.add.models.AddRecipeIngredient
import gq.kirmanak.mealient.data.add.models.AddRecipeInstruction
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import gq.kirmanak.mealient.data.add.models.AddRecipeSettings
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -17,11 +22,32 @@ class AddRecipeRepoImpl @Inject constructor(
) : AddRecipeRepo { ) : AddRecipeRepo {
override val addRecipeRequestFlow: Flow<AddRecipeRequest> override val addRecipeRequestFlow: Flow<AddRecipeRequest>
get() = addRecipeStorage.updates get() = addRecipeStorage.updates.map { it ->
AddRecipeRequest(
name = it.recipeName,
description = it.recipeDescription,
recipeYield = it.recipeYield,
recipeIngredient = it.recipeIngredients.map { AddRecipeIngredient(note = it) },
recipeInstructions = it.recipeInstructions.map { AddRecipeInstruction(text = it) },
settings = AddRecipeSettings(
public = it.isRecipePublic,
disableComments = it.areCommentsDisabled,
)
)
}
override suspend fun preserve(recipe: AddRecipeRequest) { override suspend fun preserve(recipe: AddRecipeRequest) {
Timber.v("preserveRecipe() called with: recipe = $recipe") Timber.v("preserveRecipe() called with: recipe = $recipe")
addRecipeStorage.save(recipe) val input = AddRecipeDraft(
recipeName = recipe.name,
recipeDescription = recipe.description,
recipeYield = recipe.recipeYield,
recipeInstructions = recipe.recipeInstructions.map { it.text },
recipeIngredients = recipe.recipeIngredient.map { it.note },
isRecipePublic = recipe.settings.public,
areCommentsDisabled = recipe.settings.disableComments,
)
addRecipeStorage.save(input)
} }
override suspend fun clear() { override suspend fun clear() {

View File

@@ -1,30 +0,0 @@
package gq.kirmanak.mealient.data.add.impl
import androidx.datastore.core.DataStore
import gq.kirmanak.mealient.data.add.AddRecipeStorage
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import gq.kirmanak.mealient.datastore.recipe.AddRecipeInput
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AddRecipeStorageImpl @Inject constructor(
private val dataStore: DataStore<AddRecipeInput>,
) : AddRecipeStorage {
override val updates: Flow<AddRecipeRequest>
get() = dataStore.data.map { AddRecipeRequest(it) }
override suspend fun save(addRecipeRequest: AddRecipeRequest) {
Timber.v("saveRecipeInput() called with: addRecipeRequest = $addRecipeRequest")
dataStore.updateData { addRecipeRequest.toInput() }
}
override suspend fun clear() {
Timber.v("clearRecipeInput() called")
dataStore.updateData { AddRecipeInput.getDefaultInstance() }
}
}

View File

@@ -1,6 +1,6 @@
package gq.kirmanak.mealient.data.add.models package gq.kirmanak.mealient.data.add.models
import gq.kirmanak.mealient.datastore.recipe.AddRecipeInput import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -21,27 +21,27 @@ data class AddRecipeRequest(
@SerialName("assets") val assets: List<String> = emptyList(), @SerialName("assets") val assets: List<String> = emptyList(),
@SerialName("settings") val settings: AddRecipeSettings = AddRecipeSettings(), @SerialName("settings") val settings: AddRecipeSettings = AddRecipeSettings(),
) { ) {
constructor(input: AddRecipeInput) : this( constructor(input: AddRecipeDraft) : this(
name = input.recipeName, name = input.recipeName,
description = input.recipeDescription, description = input.recipeDescription,
recipeYield = input.recipeYield, recipeYield = input.recipeYield,
recipeIngredient = input.recipeIngredientsList.map { AddRecipeIngredient(note = it) }, recipeIngredient = input.recipeIngredients.map { AddRecipeIngredient(note = it) },
recipeInstructions = input.recipeInstructionsList.map { AddRecipeInstruction(text = it) }, recipeInstructions = input.recipeInstructions.map { AddRecipeInstruction(text = it) },
settings = AddRecipeSettings( settings = AddRecipeSettings(
public = input.isRecipePublic, public = input.isRecipePublic,
disableComments = input.areCommentsDisabled, disableComments = input.areCommentsDisabled,
) )
) )
fun toInput(): AddRecipeInput = AddRecipeInput.newBuilder() fun toDraft(): AddRecipeDraft = AddRecipeDraft(
.setRecipeName(name) recipeName = name,
.setRecipeDescription(description) recipeDescription = description,
.setRecipeYield(recipeYield) recipeYield = recipeYield,
.setIsRecipePublic(settings.public) recipeInstructions = recipeInstructions.map { it.text },
.setAreCommentsDisabled(settings.disableComments) recipeIngredients = recipeIngredient.map { it.note },
.addAllRecipeIngredients(recipeIngredient.map { it.note }) isRecipePublic = settings.public,
.addAllRecipeInstructions(recipeInstructions.map { it.text }) areCommentsDisabled = settings.disableComments,
.build() )
} }
@Serializable @Serializable

View File

@@ -4,7 +4,7 @@ import android.content.SharedPreferences
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.core.content.edit import androidx.core.content.edit
import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.auth.AuthStorage
import gq.kirmanak.mealient.di.AuthModule.Companion.ENCRYPTED import gq.kirmanak.mealient.datastore.DataStoreModule.Companion.ENCRYPTED
import gq.kirmanak.mealient.extensions.prefsChangeFlow import gq.kirmanak.mealient.extensions.prefsChangeFlow
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View File

@@ -7,15 +7,15 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.data.add.AddRecipeDataSource import gq.kirmanak.mealient.data.add.AddRecipeDataSource
import gq.kirmanak.mealient.data.add.AddRecipeRepo import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.data.add.AddRecipeStorage
import gq.kirmanak.mealient.data.add.impl.AddRecipeDataSourceImpl import gq.kirmanak.mealient.data.add.impl.AddRecipeDataSourceImpl
import gq.kirmanak.mealient.data.add.impl.AddRecipeRepoImpl import gq.kirmanak.mealient.data.add.impl.AddRecipeRepoImpl
import gq.kirmanak.mealient.data.add.impl.AddRecipeService import gq.kirmanak.mealient.data.add.impl.AddRecipeService
import gq.kirmanak.mealient.data.add.impl.AddRecipeStorageImpl
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.data.network.RetrofitBuilder import gq.kirmanak.mealient.data.network.RetrofitBuilder
import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.data.network.createServiceFactory import gq.kirmanak.mealient.data.network.createServiceFactory
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorageImpl
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Named import javax.inject.Named

View File

@@ -2,9 +2,6 @@ package gq.kirmanak.mealient.di
import android.accounts.AccountManager import android.accounts.AccountManager
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@@ -32,7 +29,6 @@ import javax.inject.Singleton
interface AuthModule { interface AuthModule {
companion object { companion object {
const val ENCRYPTED = "encrypted"
@Provides @Provides
@Singleton @Singleton
@@ -49,23 +45,6 @@ interface AuthModule {
fun provideAccountManager(@ApplicationContext context: Context): AccountManager { fun provideAccountManager(@ApplicationContext context: Context): AccountManager {
return AccountManager.get(context) return AccountManager.get(context)
} }
@Provides
@Singleton
@Named(ENCRYPTED)
fun provideEncryptedSharedPreferences(
@ApplicationContext applicationContext: Context,
): SharedPreferences {
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
return EncryptedSharedPreferences.create(
ENCRYPTED,
mainKeyAlias,
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
} }
@Binds @Binds

View File

@@ -1,14 +0,0 @@
syntax = "proto3";
option java_package = "gq.kirmanak.mealient.data.add.models";
option java_multiple_files = true;
message AddRecipeInput {
string recipeName = 1;
string recipeDescription = 2;
string recipeYield = 3;
repeated string recipeInstructions = 4;
repeated string recipeIngredients = 5;
bool isRecipePublic = 6;
bool areCommentsDisabled = 7;
}

View File

@@ -1,21 +1,22 @@
package gq.kirmanak.mealient.data.add.models package gq.kirmanak.mealient.data.add.models
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
import org.junit.Test import org.junit.Test
class AddRecipeRequestTest { class AddRecipeRequestTest {
@Test @Test
fun `when construct from input then fills fields correctly`() { fun `when construct from input then fills fields correctly`() {
val input = AddRecipeInput.newBuilder() val input = AddRecipeDraft(
.setRecipeName("Recipe name") recipeName = "Recipe name",
.setRecipeDescription("Recipe description") recipeDescription = "Recipe description",
.setRecipeYield("Recipe yield") recipeYield = "Recipe yield",
.addAllRecipeIngredients(listOf("Recipe ingredient 1", "Recipe ingredient 2")) recipeInstructions = listOf("Recipe instruction 1", "Recipe instruction 2"),
.addAllRecipeInstructions(listOf("Recipe instruction 1", "Recipe instruction 2")) recipeIngredients = listOf("Recipe ingredient 1", "Recipe ingredient 2"),
.setIsRecipePublic(false) isRecipePublic = false,
.setAreCommentsDisabled(true) areCommentsDisabled = true,
.build() )
val expected = AddRecipeRequest( val expected = AddRecipeRequest(
name = "Recipe name", name = "Recipe name",
@@ -58,16 +59,16 @@ class AddRecipeRequestTest {
) )
) )
val expected = AddRecipeInput.newBuilder() val expected = AddRecipeDraft(
.setRecipeName("Recipe name") recipeName = "Recipe name",
.setRecipeDescription("Recipe description") recipeDescription = "Recipe description",
.setRecipeYield("Recipe yield") recipeYield = "Recipe yield",
.addAllRecipeIngredients(listOf("Recipe ingredient 1", "Recipe ingredient 2")) recipeInstructions = listOf("Recipe instruction 1", "Recipe instruction 2"),
.addAllRecipeInstructions(listOf("Recipe instruction 1", "Recipe instruction 2")) recipeIngredients = listOf("Recipe ingredient 1", "Recipe ingredient 2"),
.setIsRecipePublic(false) isRecipePublic = false,
.setAreCommentsDisabled(true) areCommentsDisabled = true,
.build() )
assertThat(request.toInput()).isEqualTo(expected) assertThat(request.toDraft()).isEqualTo(expected)
} }
} }

View File

@@ -1,9 +1,12 @@
package gq.kirmanak.mealient.datastore package gq.kirmanak.mealient.datastore
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile import androidx.datastore.dataStoreFile
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -11,6 +14,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.datastore.recipe.AddRecipeInput import gq.kirmanak.mealient.datastore.recipe.AddRecipeInput
import gq.kirmanak.mealient.datastore.recipe.AddRecipeInputSerializer import gq.kirmanak.mealient.datastore.recipe.AddRecipeInputSerializer
import javax.inject.Named
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -18,6 +22,8 @@ import javax.inject.Singleton
interface DataStoreModule { interface DataStoreModule {
companion object { companion object {
const val ENCRYPTED = "encrypted"
@Provides @Provides
@Singleton @Singleton
fun provideAddRecipeInputStore( fun provideAddRecipeInputStore(
@@ -25,5 +31,22 @@ interface DataStoreModule {
): DataStore<AddRecipeInput> = DataStoreFactory.create(AddRecipeInputSerializer) { ): DataStore<AddRecipeInput> = DataStoreFactory.create(AddRecipeInputSerializer) {
context.dataStoreFile("add_recipe_input") context.dataStoreFile("add_recipe_input")
} }
@Provides
@Singleton
@Named(ENCRYPTED)
fun provideEncryptedSharedPreferences(
@ApplicationContext applicationContext: Context,
): SharedPreferences {
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
return EncryptedSharedPreferences.create(
ENCRYPTED,
mainKeyAlias,
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
} }
} }

View File

@@ -0,0 +1,11 @@
package gq.kirmanak.mealient.datastore.recipe
data class AddRecipeDraft(
val recipeName: String,
val recipeDescription: String,
val recipeYield: String,
val recipeInstructions: List<String>,
val recipeIngredients: List<String>,
val isRecipePublic: Boolean,
val areCommentsDisabled: Boolean,
)

View File

@@ -0,0 +1,12 @@
package gq.kirmanak.mealient.datastore.recipe
import kotlinx.coroutines.flow.Flow
interface AddRecipeStorage {
val updates: Flow<AddRecipeDraft>
suspend fun save(addRecipeRequest: AddRecipeDraft)
suspend fun clear()
}

View File

@@ -0,0 +1,43 @@
package gq.kirmanak.mealient.datastore.recipe
import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AddRecipeStorageImpl @Inject constructor(
private val dataStore: DataStore<AddRecipeInput>,
) : AddRecipeStorage {
override val updates: Flow<AddRecipeDraft>
get() = dataStore.data.map {
AddRecipeDraft(
recipeName = it.recipeName,
recipeDescription = it.recipeDescription,
recipeYield = it.recipeYield,
recipeInstructions = it.recipeInstructionsList,
recipeIngredients = it.recipeIngredientsList,
isRecipePublic = it.isRecipePublic,
areCommentsDisabled = it.areCommentsDisabled,
)
}
override suspend fun save(addRecipeRequest: AddRecipeDraft) {
val input = AddRecipeInput.newBuilder()
.setRecipeName(addRecipeRequest.recipeName)
.setRecipeDescription(addRecipeRequest.recipeDescription)
.setRecipeYield(addRecipeRequest.recipeYield)
.setIsRecipePublic(addRecipeRequest.isRecipePublic)
.setAreCommentsDisabled(addRecipeRequest.areCommentsDisabled)
.addAllRecipeIngredients(addRecipeRequest.recipeIngredients)
.addAllRecipeInstructions(addRecipeRequest.recipeInstructions)
.build()
dataStore.updateData { input }
}
override suspend fun clear() {
dataStore.updateData { AddRecipeInput.getDefaultInstance() }
}
}