I have designed a layout that includes recipe details and three CTAs in the app bar to share, delete, and edit recipes. I need feedback regarding the architecture of my Compose UI.
RecipeDetailFragment
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
val args = arguments?.let { RecipeDetailFragmentArgs.fromBundle(it) }
args?.bundleRecipe?.let {
viewModel.createPaletteAsync(it)
setContent {
RecipeDetailBase(
recipe = it,
viewModel = viewModel,
onNavigateToRecipeListScreen = {
activity?.onBackPressed()
},
onNavigateToRecipeUpdateScreen = {
val action =
RecipeDetailFragmentDirections.actionRecipeDetailFragmentToRecipeUpdateFragment(
it
)
findNavController().navigate(action)
},
onShareRecipe = {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, viewModel.getRecipeText(it))
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
},
sharedPreferences = sharedPreferences
)
}
}
}
}
RecipeDetailBase
@OptIn(FlowPreview::class)
@ExperimentalCoroutinesApi
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun RecipeDetailBase(
recipe: Recipe,
viewModel: RecipeDetailViewModel,
onNavigateToRecipeListScreen: () -> Unit,
onNavigateToRecipeUpdateScreen: () -> Unit,
onShareRecipe: () -> Unit,
sharedPreferences: SharedPreferences
) {
RecipeDetailScreen(
recipe = recipe,
setShouldDialogOpen = { value -> viewModel.shouldDialogOpen.value = value },
onNavigateToRecipeListScreen = onNavigateToRecipeListScreen,
onNavigateToRecipeUpdateScreen = onNavigateToRecipeUpdateScreen,
onShareRecipe = onShareRecipe,
userId = sharedPreferences.getString(
PreferenceKeys.USER_UID, null
),
titleColor = viewModel.titleColor,
headingsColor = viewModel.headingsColor,
textColor = viewModel.textColor,
formatSteps = { steps -> viewModel.formatSteps(steps = steps) },
shouldDialogOpenValue = viewModel.shouldDialogOpen.value,
beginDelete = { recipeToBeDeleted -> viewModel.beginDelete(recipe = recipeToBeDeleted) }
)
}
RecipeDetailScreen
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun RecipeDetailScreen(
recipe: Recipe,
setShouldDialogOpen: (Boolean) -> Unit,
onNavigateToRecipeListScreen: () -> Unit,
onNavigateToRecipeUpdateScreen: () -> Unit,
onShareRecipe: () -> Unit,
userId: String?,
titleColor: MutableState<Int?>,
headingsColor: MutableState<Int?>,
textColor: MutableState<Int?>,
formatSteps: (List<String>) -> ArrayList<String>,
shouldDialogOpenValue: Boolean,
beginDelete: (Recipe) -> Unit
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = {
Text(text = "Recipe Details")
},
navigationIcon = {
IconButton(onClick = {
onNavigateToRecipeListScreen()
}) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back"
)
}
},
actions = {
IconButton(onClick = {
onShareRecipe()
}) {
Icon(
imageVector = Icons.Filled.Share,
contentDescription = "Share"
)
}
if (TextUtils.isEmpty(recipe.userId) || recipe.userId.equals(
userId
) || ("ryz") == userId
) {
IconButton(onClick = {
onNavigateToRecipeUpdateScreen()
}) {
Icon(
imageVector = Icons.Filled.Edit,
contentDescription = "Edit"
)
}
IconButton(onClick = {
setShouldDialogOpen(true)
}) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Delete"
)
}
}
},
backgroundColor = colorResource(id = R.color.colorPrimaryDark),
contentColor = Color.White,
elevation = 2.dp
)
},
content = {
RecipeDetailContent(
recipe,
titleColor = titleColor,
headingsColor = headingsColor,
textColor = textColor,
formatSteps = formatSteps,
shouldDialogOpenValue = shouldDialogOpenValue,
setShouldDialogOpen = setShouldDialogOpen,
beginDelete = beginDelete
)
})
}
@SuppressLint("UnrememberedMutableState")
@Preview
@Composable
fun RecipeDetailScreenPreview() {
RecipeDetailScreen(
recipe = Recipe(
id = "test",
title = "Chia Pudding Recipe (Easy & Healthy)",
ingredients = "Chia Seeds – You’ll need chia seeds to make a proper chia pudding. That said, linseeds (a.k.a. flaxseeds) will act similarly.\n",
steps = "Add 2 tablespoons of chia seeds to a glass or jar. If you want you can rinse the seeds with water in a superfine strain",
imageUrl = null,
updated_at = "test",
created_at = "test",
description = "Chia seed pudding is made by soaking chia seeds in liquid like milk, nut milk, or juice for at least 2 hours or overnight. The chia seeds absorb the liquid and become gelatinous, creating a texture that’s similar to tapioca pudding, but with smaller pieces.",
preparationTime = "30 minutes",
nutritionInfo = "370 kcal",
notes = "loved it",
socialNetworkState = false,
servings = "2 persons",
headerImage = "https://www.vegrecipesofindia.com/wp-content/uploads/2020/01/chia-pudding.jpg"
),
setShouldDialogOpen = {},
onNavigateToRecipeListScreen = {},
onNavigateToRecipeUpdateScreen = {},
onShareRecipe = {},
userId = "test",
titleColor = mutableStateOf(null),
headingsColor = mutableStateOf(null),
textColor = mutableStateOf(null),
formatSteps = { ArrayList() },
shouldDialogOpenValue = false,
beginDelete = {}
)
}
RecipeDetailContent
@Composable
fun RecipeDetailContent(
recipe: Recipe,
titleColor: MutableState<Int?>,
headingsColor: MutableState<Int?>,
textColor: MutableState<Int?>,
formatSteps: (List<String>) -> ArrayList<String>,
shouldDialogOpenValue: Boolean,
setShouldDialogOpen: (Boolean) -> Unit,
beginDelete: (Recipe) -> Unit
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.fillMaxHeight()
) {
if (!TextUtils.isEmpty(recipe.headerImage)) {
Image(
painter = rememberImagePainter(recipe.headerImage,
builder = {
size(OriginalSize)
placeholder(R.drawable.ic_placeholder)
}),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
}
Text(
text = recipe.title,
style = titleColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 20.sp, color = it) } ?: TextStyle(fontSize = 20.sp),
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
if (!TextUtils.isEmpty(recipe.description)) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Description",
style = headingsColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 20.sp, color = it) }
?: TextStyle(fontSize = 20.sp),
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = recipe.description!!,
style = textColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 18.sp, color = it) }
?: TextStyle(fontSize = 18.sp),
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
}
if (!TextUtils.isEmpty(recipe.servings)) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Servings",
style = headingsColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 20.sp, color = it) }
?: TextStyle(fontSize = 20.sp),
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = recipe.servings!!,
style = textColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 18.sp, color = it) }
?: TextStyle(fontSize = 18.sp),
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
}
if (!TextUtils.isEmpty(recipe.ingredients)) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Ingredients",
style = headingsColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 20.sp, color = it) }
?: TextStyle(fontSize = 20.sp),
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = recipe.ingredients!!,
style = textColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 18.sp, color = it) }
?: TextStyle(fontSize = 18.sp),
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
}
if (!TextUtils.isEmpty(recipe.preparationTime)) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Preparation Time",
style = headingsColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 20.sp, color = it) }
?: TextStyle(fontSize = 20.sp),
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = recipe.preparationTime!!,
style = textColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 18.sp, color = it) }
?: TextStyle(fontSize = 18.sp),
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
}
if (!TextUtils.isEmpty(recipe.nutritionInfo)) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Nutrition Info",
style = headingsColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 20.sp, color = it) }
?: TextStyle(fontSize = 20.sp),
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = recipe.nutritionInfo!!,
style = textColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 18.sp, color = it) }
?: TextStyle(fontSize = 18.sp),
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Preparation",
style = headingsColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 20.sp, color = it) } ?: TextStyle(fontSize = 20.sp),
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
Spacer(modifier = Modifier.height(16.dp))
val stepList = formatSteps(recipe.steps.split("\n"))
val images = recipe.imageUrl?.split("\n")
stepList.forEachIndexed { index, item ->
images?.forEachIndexed { _, element ->
val pos = index.toString()
if (element.startsWith(pos, true)) {
Image(
painter = rememberImagePainter(
element.replace(pos.plus(" . "), ""),
builder = {
size(OriginalSize)
placeholder(R.drawable.ic_placeholder)
}),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
)
}
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
elevation = 2.dp,
backgroundColor = Color(0xffdbffdb),
contentColor = Color.Black
) {
Text(
text = item,
style = textColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 18.sp, color = it) }
?: TextStyle(fontSize = 18.sp),
modifier = Modifier
.align(Alignment.Start)
.padding(8.dp)
)
}
}
if (!TextUtils.isEmpty(recipe.notes)) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Notes",
style = headingsColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 20.sp, color = it) }
?: TextStyle(fontSize = 20.sp),
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(18.dp)
.align(Alignment.Start)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = recipe.notes!!,
style = textColor.value?.let { Color(it) }
?.let { TextStyle(fontSize = 18.sp, color = it) }
?: TextStyle(fontSize = 18.sp),
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
}
}
AlertDialogComponent(
recipe = recipe,
shouldDialogOpenValue = shouldDialogOpenValue,
setShouldDialogOpen = setShouldDialogOpen,
beginDelete = beginDelete
)
}
@SuppressLint("UnrememberedMutableState")
@Preview
@Composable
fun RecipeDetailContentPreview() {
RecipeDetailContent(
recipe = Recipe(
id = "test",
title = "Chia Pudding Recipe (Easy & Healthy)",
ingredients = "Chia Seeds – You’ll need chia seeds to make a proper chia pudding. That said, linseeds (a.k.a. flaxseeds) will act similarly.\n",
steps = "Add 2 tablespoons of chia seeds to a glass or jar. If you want you can rinse the seeds with water in a superfine strain",
imageUrl = null,
updated_at = "test",
created_at = "test",
description = "Chia seed pudding is made by soaking chia seeds in liquid like milk, nut milk, or juice for at least 2 hours or overnight. The chia seeds absorb the liquid and become gelatinous, creating a texture that’s similar to tapioca pudding, but with smaller pieces.",
preparationTime = "30 minutes",
nutritionInfo = "370 kcal",
notes = "loved it",
socialNetworkState = false,
servings = "2 persons",
headerImage = "https://www.vegrecipesofindia.com/wp-content/uploads/2020/01/chia-pudding.jpg"
),
titleColor = mutableStateOf(null),
headingsColor = mutableStateOf(null),
textColor = mutableStateOf(null),
formatSteps = { ArrayList() },
shouldDialogOpenValue = false,
setShouldDialogOpen = {},
beginDelete = {}
)
}
AlertDialogComponent
@Composable
fun AlertDialogComponent(
recipe: Recipe,
shouldDialogOpenValue: Boolean,
setShouldDialogOpen: (Boolean) -> Unit,
beginDelete: (Recipe) -> Unit
) {
if (shouldDialogOpenValue) {
AlertDialog(onDismissRequest = { setShouldDialogOpen(false) },
title = {
Text(
text = "Delete Recipe?",
style = TextStyle(fontSize = 20.sp),
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.primaryText)
)
},
text = {
Text(
"Are you sure you want to delete this Recipe?",
style = TextStyle(fontSize = 18.sp),
color = colorResource(id = R.color.secondaryText)
)
},
dismissButton = {
Button(
modifier = Modifier.fillMaxWidth(), onClick = {
setShouldDialogOpen(false)
},
colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(id = R.color.colorAccent))
) {
Text(
text = "Cancel",
style = TextStyle(fontSize = 16.sp)
)
}
}, confirmButton = {
Button(
modifier = Modifier.fillMaxWidth(), onClick = {
setShouldDialogOpen(false)
beginDelete(recipe)
},
colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(id = R.color.colorAccent))
) {
Text(
text = "Okay",
style = TextStyle(fontSize = 16.sp)
)
}
})
}
}
@Preview
@Composable
fun AlertDialogComponentPreview() {
AlertDialogComponent(
recipe = Recipe(
id = "test",
title = "Chia Pudding Recipe (Easy & Healthy)",
ingredients = "Chia Seeds – You’ll need chia seeds to make a proper chia pudding. That said, linseeds (a.k.a. flaxseeds) will act similarly.\n",
steps = "Add 2 tablespoons of chia seeds to a glass or jar. If you want you can rinse the seeds with water in a superfine strain",
imageUrl = null,
updated_at = "test",
created_at = "test",
description = "Chia seed pudding is made by soaking chia seeds in liquid like milk, nut milk, or juice for at least 2 hours or overnight. The chia seeds absorb the liquid and become gelatinous, creating a texture that’s similar to tapioca pudding, but with smaller pieces.",
preparationTime = "30 minutes",
nutritionInfo = "370 kcal",
notes = "loved it",
socialNetworkState = false,
servings = "2 persons",
headerImage = "https://www.vegrecipesofindia.com/wp-content/uploads/2020/01/chia-pudding.jpg"
),
shouldDialogOpenValue = true,
setShouldDialogOpen = {},
beginDelete = {}
)
}