1
\$\begingroup\$

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 = {}
    )
}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ When reviewing UI code, adding screenshots for how it looks would help \$\endgroup\$ Commented Jun 17, 2023 at 23:29

0