Skip to content

Commit

Permalink
General changes to repositories and ViewModel designs. Add field for …
Browse files Browse the repository at this point in the history
…holding recipe ingredients to EditRecipeScreen.
  • Loading branch information
Cody Weaver authored and Cody Weaver committed Feb 4, 2024
1 parent 9ff701f commit 32b6485
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,32 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.android.pocketalchemy.R
import com.android.pocketalchemy.model.Recipe
import com.android.pocketalchemy.model.RecipeIngredient
import com.android.pocketalchemy.ui.common.PaNavBar
import com.android.pocketalchemy.ui.common.PaTopAppBar

Expand All @@ -34,7 +45,8 @@ private const val MAX_DESCRIPTION_HEIGHT = 100
/**
* Screen for creating and editing recipes.
* @param navController NavController for current NavHost
* @param editRecipeViewModel EditRecipeViewModel - should be initialized with setRecipeId()
* @param editRecipeViewModel EditRecipeViewModel - should be initialized with
* [EditRecipeViewModel.setRecipe]
*/
@Composable
fun EditRecipeScreen(
Expand All @@ -58,54 +70,67 @@ fun EditRecipeScreen(
Column(
modifier = Modifier.padding(scaffoldPadding)
) {
val recipe = editRecipeUiState.value.recipe

TitleField(
recipe = recipe,
onUpdate = {
editRecipeViewModel.updateUiState(
recipe = recipe.copy(title = it)
)
if (editRecipeViewModel.isLoading) {
Surface(
color = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.fillMaxSize(1f)
) {
Box {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
)
} else {
val recipe = editRecipeUiState.value.recipe

DescriptionField(
recipe = recipe,
onUpdate = {
editRecipeViewModel.updateUiState(
recipe = recipe.copy(description = it)
)
}
)
TitleField(
recipe = recipe,
onUpdate = {
editRecipeViewModel.updateUiState(
recipe = recipe.copy(title = it)
)
}
)

Row( // Ingredients
DescriptionField(
recipe = recipe,
onUpdate = {
editRecipeViewModel.updateUiState(
recipe = recipe.copy(description = it)
)
}
)

) { /* TODO: */
val ingredients = editRecipeUiState.value.ingredients
Log.d(TAG, ingredients.toString())
}
Row { /* TODO: */
val ingredients = editRecipeUiState.value.ingredients
Log.d(TAG, ingredients.toString())
IngredientsField(ingredients)
}

Row( // Recipe Instructions
Row( // Recipe Instructions

) { /* TODO: */ }
) { /* TODO: */ }

// Cancel and Save buttons
Row(
modifier = Modifier.padding(4.dp)
) {
SaveButton(
onClick = {
editRecipeViewModel.saveRecipe()
navController.popBackStack()
}
)
// Cancel and Save buttons
Row(
modifier = Modifier.padding(4.dp)
) {
SaveButton(
onClick = {
editRecipeViewModel.saveRecipe()
navController.popBackStack()
}
)

BackButton(
onClick = {
editRecipeViewModel.clearRecipeId()
navController.popBackStack()
}
)
BackButton(
onClick = {
editRecipeViewModel.clearRecipeId()
navController.popBackStack()
}
)
}
}
}
}
Expand Down Expand Up @@ -141,7 +166,7 @@ private fun TitleField(
}

/**
* Draws description field and requests updates from view model.
* Draws description field and requests updates to state from view model.
*/
@Composable
private fun DescriptionField(
Expand Down Expand Up @@ -176,6 +201,54 @@ private fun DescriptionField(
}
}

@Composable
fun IngredientsField(
ingredients: List<RecipeIngredient>
) {
Column (
Modifier
.padding(8.dp)
.fillMaxWidth(1f)
){
Row {
Column(
modifier = Modifier.align(Alignment.CenterVertically)
) {
Text(
text = stringResource(id = R.string.ingredients_label),
modifier = Modifier.align(Alignment.Start),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.displayLarge,
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
}
Column (
modifier = Modifier.align(Alignment.CenterVertically)
) {
IconButton(
onClick = { /*TODO*/ },
) {
Icon(
painter = painterResource(id = R.drawable.plus),
contentDescription = stringResource(id = R.string.plus_description)
)
}
}
}

Row {
LazyColumn(userScrollEnabled = false) {
for (ingredient in ingredients) {
item {

}
}
}
}
}
}

/**
* Save Recipe Button
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ import com.android.pocketalchemy.model.RecipeIngredient
*/
data class EditRecipeUiState(
var recipe: Recipe,
val ingredients: List<RecipeIngredient>
val ingredients: List<RecipeIngredient>,
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.android.pocketalchemy.editrecipe

import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
Expand All @@ -9,17 +8,16 @@ import com.android.pocketalchemy.model.RecipeIngredient
import com.android.pocketalchemy.repository.AuthRepository
import com.android.pocketalchemy.repository.RecipeIngredientRepository
import com.android.pocketalchemy.repository.RecipeRepository
import com.google.firebase.firestore.toObject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

private const val TAG = "EditRecipeViewModel"

/**
* ViewModel for EditRecipeScreen
*/
Expand All @@ -30,7 +28,9 @@ class EditRecipeViewModel @Inject constructor(
private val recipeIngredientRepository: RecipeIngredientRepository,
private val authRepository: AuthRepository,
) : ViewModel() {
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
private var _isLoading = false
val isLoading
get() = _isLoading
/**
* Retrieves recipe id from SavedStateHandle
*/
Expand Down Expand Up @@ -63,39 +63,32 @@ class EditRecipeViewModel @Inject constructor(

/**
* Initializes view model with recipe from recipeId.
* Has no effect if already set.
* @param recipeId used to retrieve recipe document
*/
fun setRecipe(recipeId: String?) {
viewModelScope.launch(
ioDispatcher
) {
val recipeDoc = recipeRepository.getRecipe(recipeId)
savedStateHandle[EDIT_RECIPE_ID_KEY] = recipeDoc.id

recipeDoc.get()
.addOnSuccessListener { snapshot ->
snapshot.toObject<Recipe>()?.let { recipeSnapshot ->
updateUiState(recipe = recipeSnapshot)
}
}
.addOnFailureListener {
Log.w(TAG, "Cannot get recipe with id: $recipeId, ex: $it")
}
viewModelScope.launch {
_isLoading = true
val recipeDocRef = recipeRepository.getRecipeDocRef(recipeId)
savedStateHandle[EDIT_RECIPE_ID_KEY] = recipeDocRef.id

// sets initial recipe details
val recipe = recipeRepository.getRecipe(recipeDocRef)

// sets initial ingredient list
recipeIngredientRepository.getRecipeIngredients(recipeId.toString()) {
updateUiState(recipeIngredients = it)
}
val ingredients = recipeIngredientRepository.getRecipeIngredients(recipeId.toString())

updateUiState(recipe, ingredients)
_isLoading = false
}
}

/**
* updates EditRecipeUiState
* Updates the state of the recipe and its ingredients being
* edited.
*/
fun updateUiState(
recipe: Recipe? = null,
recipeIngredients: List<RecipeIngredient>? = null
recipeIngredients: List<RecipeIngredient>? = null,
) {
viewModelScope.launch {
recipe?.let { newRecipe ->
Expand All @@ -119,15 +112,43 @@ class EditRecipeViewModel @Inject constructor(
savedStateHandle[EDIT_RECIPE_ID_KEY] = null
}

/**
* Adds a given RecipeIngredient to a recipe, if the recipe already contains
* a RecipeIngredient with the same ingredientId, the gram weights are summed
* and the ingredient list is updated.
* @param recipeIngredient ingredient being added
*/
fun addRecipeIngredient(recipeIngredient: RecipeIngredient) {
viewModelScope.launch {
val recipeIngredients = _editRecipeUiState.value.ingredients
val ingredientIndex = recipeIngredients.indexOfFirst {
it.ingredientId == recipeIngredient.ingredientId
}
val newRecipeIngredients = if (ingredientIndex == NOT_FOUND) {
recipeIngredients + recipeIngredient
}
else {
recipeIngredients.mapIndexed { i: Int, ingredient: RecipeIngredient ->
if (i == ingredientIndex) {
ingredient.copy(
gramWeight = ingredient.gramWeight + recipeIngredient.gramWeight
)
} else {
ingredient
}
}
}
updateUiState(
recipeIngredients = recipeIngredients + recipeIngredient
recipeIngredients = newRecipeIngredients
)
}
}

/**
* Removes RecipeIngredient and tracks it if it needs to be
* deleted from firestore on call to [saveRecipe]
* @param recipeIngredient ingredient being removed
*/
fun removeRecipeIngredient(recipeIngredient: RecipeIngredient) {
viewModelScope.launch {
val recipeIngredients = _editRecipeUiState.value.ingredients
Expand All @@ -136,6 +157,11 @@ class EditRecipeViewModel @Inject constructor(
it.ingredientId != recipeIngredient.id
}
)
if (recipeIngredient.id != null) {
// Tracks ingredients that need to be deleted
// from firestore on recipe save
recipeIngredientsToDelete.add(recipeIngredient)
}
}
}

Expand All @@ -144,16 +170,18 @@ class EditRecipeViewModel @Inject constructor(
*/
fun saveRecipe() {
// TODO: Check no required fields are empty!!!
viewModelScope.launch(
ioDispatcher
) {
recipeRepository.setRecipe(_editRecipeUiState.value.recipe)
viewModelScope.launch {
recipeRepository.setRecipe(
// Recipe id is auto filled with doc id so must copy auto
// id before setting.
_editRecipeUiState.value.recipe.copy(id = recipeId)
)
clearRecipeId()
}
}

companion object {
private const val TAG = "EditRecipeViewModel"
private const val NOT_FOUND = -1
private const val EDIT_RECIPE_ID_KEY = "edit-recipe-id-key"
}
}
Loading

0 comments on commit 32b6485

Please sign in to comment.