How to do state hoist of two or three levels?

57 Views Asked by At

Imagine a composable A that haves a lower composable B that haves a even lower composable C with haves some buttons.

The buttons must change a state remember variable stored in composable A. The state hoist theory tells you that you must pass, for each button, the variable value and the lambda function that modifies the state variable in two parameters from A to B, then, from B to C. But if there are 10 buttons, you will need to pass a lot of lambdas and parameters, it seems to be overprogramming and very complex.

Is there a better way to achieve this?

This is a sample code similar to that A B C hierarchy, in which the Button composables needs to change the state var currentImageModel in the main composable ImagePortfolio

@Composable
fun ImagePortfolio(modifier: Modifier = Modifier) {
    val currentImageModel by remember { mutableStateOf(0) }

    Column(
        modifier = modifier
            .fillMaxSize()
            .padding(24.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        ImageHolder(
            ImageModelProvider.imageModelList[currentImageModel].imageResource,
            modifier.weight(0.75f).wrapContentSize()
        )
        Column(
            modifier.weight(0.25f).fillMaxWidth(),
            verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Bottom),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            DescriptionHolder(
                ImageModelProvider.imageModelList[currentImageModel].imageDescription,
                ImageModelProvider.imageModelList[currentImageModel].imageAuthor,
                ImageModelProvider.imageModelList[currentImageModel].imageYear.toString()
            )
            ButtonsHolder()
        }
    }
}

@Composable
fun ImageHolder(imageResource: Int, modifier: Modifier = Modifier) {
    Image(
        painter = painterResource(id = imageResource),
        contentDescription = null,
        modifier = modifier
            .border(BorderStroke(16.dp, Color.White))
            .padding(16.dp)
            .shadow(elevation = 20.dp)
    )
}

@Composable
fun DescriptionHolder(description: String, author: String, year: String, modifier: Modifier = Modifier) {
    Column (
        modifier = modifier
            .fillMaxWidth(0.8f)
            .background(colorResource(R.color.description_background))
            .padding(8.dp)
    ) {
        Text(
            text = description
        )
        Row (
            horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start)
        ) {
            Text(
                text = author,
                fontWeight = FontWeight.Bold
            )
            Text(
                text = "($year)"
            )
        }
    }
}

@Composable
fun ButtonsHolder(modifier: Modifier = Modifier) {
    Row (
        modifier = modifier
            .fillMaxWidth(0.9f)
            .padding(4.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ){
        Button(R.string.previous, {}, Modifier.width(120.dp))
        Button(R.string.next, {}, Modifier.width(120.dp))
    }
}

@Composable
fun Button(textResource: Int, function: (()-> Unit), modifier: Modifier = Modifier) {
    Button(
        modifier = modifier,
        colors = ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.button_color)),
        onClick = function
    ) {
        Text(text = stringResource(id = textResource))
    }
}
2

There are 2 best solutions below

0
Emmanuel Montt On

lambdas functions can trigger recomposition. it's better use a "data class" with "remembers" to avoid it.

actions class contains all event and listeners of screen.

data class PortfolioActions(
    val previous: () -> Unit = {},
    val next: () -> Unit = {},
    val onClick: () -> Unit = {}
)

@Composable
fun rememberExchangeCBUDetailActions(
    viewModel: ViewModel
): PortfolioActions =
    remember {
        PortfolioActions(
            previous = viewmodel::doSomething()
        )
    }

@Composable
fun ButtonsHolder(
    modifier: Modifier = Modifier,
    actions: PortfolioActions
) {
    Row(
        modifier = modifier
            .fillMaxWidth(0.9f)
            .padding(4.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Button(R.string.previous, actions.previous, Modifier.width(120.dp))
        Button(R.string.next, actions.next, Modifier.width(120.dp))
    }
}

@Composable
fun Button(
    modifier: Modifier = Modifier,
    textResource: Int,
    actions: PortfolioActions
) {
    Button(
        modifier = modifier,
        colors = ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.button_color)),
        onClick = actinons.onClick
    ) {
        Text(text = stringResource(id = textResource))
    }
}

About DescriptionHolder you can use a immutable class

data class Description (
  val description: String,
  val author: String, 
  val year: String
)

@Composable
fun DescriptionHolder(description: Description, modifier: Modifier = Modifier) {}

Important use "val" and not "var"

Documentation:

https://developer.android.com/jetpack/compose/performance/stability

and good practices

https://www.youtube.com/watch?v=EVVFhyuVV5g

3
Tenfour04 On

In this case, I think I would use an enum to represent the possible button clicks. Then only one click listener has to be hoisted up.

enum class PortfolioClick {
    Next, Previous
}
@Composable
fun ImagePortfolio(modifier: Modifier = Modifier) {
    val currentImageModel by remember { mutableStateOf(0) }

    Column(
        modifier = modifier
            .fillMaxSize()
            .padding(24.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        ImageHolder(
            ImageModelProvider.imageModelList[currentImageModel].imageResource,
            modifier.weight(0.75f).wrapContentSize()
        )
        Column(
            modifier.weight(0.25f).fillMaxWidth(),
            verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Bottom),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            DescriptionHolder(
                ImageModelProvider.imageModelList[currentImageModel].imageDescription,
                ImageModelProvider.imageModelList[currentImageModel].imageAuthor,
                ImageModelProvider.imageModelList[currentImageModel].imageYear.toString()
            )
            ButtonsHolder {
                when (it) {
                    PortfolioClick.Previous -> // do something
                    PortfolioClick.Next -> // ...
                }
            }
        }
    }
}

// ...

@Composable
fun ButtonsHolder(modifier: Modifier = Modifier, clickListener: (PortfolioClick)->Unit = {}) {
    Row (
        modifier = modifier
            .fillMaxWidth(0.9f)
            .padding(4.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ){
        Button(R.string.previous, { clickListener(PortfolioClick.Previous) }, Modifier.width(120.dp))
        Button(R.string.next, { clickListener(PortfolioClick.Next) }, Modifier.width(120.dp))
    }
}

// ...