Leave a rating/review
In the previous episode, you triggered the update of the alertIsVisible state object that is defined in the GameScreen composable from an event inside the alert dialog.
This pattern of making the ResultDialog a stateless composable and lifting the state to the parent GameScreen composable
is known as “State Hoisting.”
We do this in order to have reusable stateless composables that dont need to manage the logic of updating state. A Stateless composable is just one that doesnt have a state variable defined locally inside it. It is the arguments passed to it that determines how its data is managed.
A stateless composable function that wants to use the state hoisting pattern should replace the local state with these two parameters:
-
value: T: which is the current value to display and T here is the data type of the value, and -
onValueChange: (T) -> Unit: which is a lambda that represents the event that requests the value to change, whereTis the proposed new value that is passed up to the parent composable.
Now, the state hoisting pattern has many benefits and some of which are:
-
it ensures a single source of truth. Since the state variable is declared only in the parent composable, we know that it will be the only value that will be updated since there’re no duplicate objects created elsewhere and this helps avoid bugs.
-
Only stateful composables can modify their state and this happens where the stateless composable is called.
-
State that is hoisted can be shared with different composables because the state is defined in the stateful parent composable. With this, the state’s value can easily be passed around to different stateless children composables inside it.
-
The state can be modified or ignored where it is called and this is possible because of the event lambda. So in the
ResultDialogexample, we could ignore the state update by passing in an empty lambda. -
The stateless composable doesn’t care where the state is coming from as long as you pass in the value with the expected data type. So the state can come from Jetpack Compose mutable state object or maybe from another state management library.
The ResultDialog composable is not an excellent example of state hoisting as it just has a lambda that calls a function.
So in this episode, you’ll use state hoisting to implement a stateless TargetSlider composable that exhibits all these qualities.
With this, you’ll have a better understanding of how state hoisting works.
Alright, let’s get started.
If you run your app and try moving the slider’s thumb, you’ll see that it does not update.
Remember, you’ve not attached any state to it so Jetpack Compose would not be able to recompose the UI with the updated thumb position.
This update is done in the onValueChanged lambda and you can see we passed in an empty block so nothing happens.
But before we add any state, if you examine the slider code, it looks like a good candidate to make it a reusable widget. You might want to use it as a range slider in some othe part of your app. I know, this is a small app but I want you to start thinking in terms of having small reusable stateless composables and having a stateful composable at the top.
Alright, go ahead and create a new Kotlin file by right clicking the reverse domain name in the project tree.
Then go to New, Kotlin Class/File, select file, give it a name of TargetSlider then hit the return key.
Create an empty TargetSlider composable with a default modifier like so:
import androidx.compose.runtime.Composable
@Composable
fun TargetSlider(modifier: Modifier = Modifier) {
}
Head over to the GameScreen file.
Cut the code from the Row and all its contents.
Head back to the TargetSlider file.
Then paste it in as the content of the composable:
@Composable
fun TargetSlider(modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier // Make sure you add this
) {
Text(
stringResource(id = R.string.min_value_text),
textAlign = TextAlign.Center,
modifier = Modifier.padding(start = 16.dp)
)
Slider(
value = 0.5f,
valueRange = 0.01f..1f,
onValueChange = { },
modifier = Modifier.weight(1f)
)
Text(
stringResource(id = R.string.max_value_text),
textAlign = TextAlign.Center,
modifier = Modifier.padding(end = 16.dp)
)
}
}
And make sure you add the modifier to the Row just in case you want to be able to customize it where you use this widget.
Head back to the GameScreen composable.
Then call in TargetSlider composable where you cut out the Row, and thats in between the target text and the hit me button.
Remember as you type in the name, Android Studio’s intellisense will kick in and you just need to select the composable from the list and hit return so it also imports the function at the top.
TargetSlider()
Let’s head back to the TargetSlider file.
Now using the knowledge of state hoisting from the previous episode, we want the state logic to still be handled by the stateful GameScreen composable.
So the TargetSlider composable will need the slider value and also a way to update the current selected value from inside this composable.
First, lets add the value paramater to the function and give it a default value to prevent an error in case an initial value is
not passed where it is used:
fun TargetSlider(modifier: Modifier = Modifier, value: Float = 0.5f) {
//...
}
Then replace the value argument in the Slider with this:
Slider(
value = value,
//...
)
Next, you need to get the value from the Slider whenever it changes, that is, when the player moves the slider’s thumb.
This is done in the onValueChange argument which is a lambda but since the state would not reside in the TargetSlider composable, we need to put a lambda that will be triggered from where this composable will be used.
Currently we pass in an empty block but let’s see the definition of what this lambda looks like.
Hover over the argument.
And you can see the Slider composable passes a float value as a parameter and the lambda returns nothing.
This is exactly how the parmeter for the TargetSlider will look like so lets copy it, then add a valueChanged parameter to the TargetSlider.
After that, paste it in as the type:
@Composable
fun TargetSlider(
modifier: Modifier = Modifier
value: Float = 0.5f,
valueChanged: (Float) -> Unit, // New Code
) {
//...
}
These are the two parameters we talked about that are essential for state hoisting: the value and the valueChanged lambda.
You can name them anything but just know that the value will be a noun while the event will be a verb because it is called whenever an action occurs in this case, moving the slider’s thumb.
Finally, pass this parameter to the onValueChanged argument of the Slider.
onValueChange = valueChanged,
This is a perfect example of state hoisting. This composable can be used anywhere irrespective of how the slider value is gotten. It is a fully independent widget that can accept a value and also be able to update that value without having to define the state internally.
We’re done with the custom TargetSlider composable.
Let’s head back to the GameScreen.kt file.
First you need to define the sliderValue state.
Enter the following code under the alertIsVisible state object:
var sliderValue by remember { mutableStateOf(0.5f) }
Then scroll down to where the TargetSlider is called.
You can see that we already have an error and if you hover over it, it says “No value passed for parameter ‘valueChanged’.”
Remember, you gave a default value for the value parameter of TargetSlider, so you dont get an error for that.
Now, this makes sense because what’s the point of a slider if you dont implement a way to update the value when you move the thumb?
Alright, pass in the following code to it:
TargetSlider(
value = sliderValue,
valueChanged = { value ->
sliderValue = value
}
)
First, you set the value to the sliderValue gotten from the state object.
Next, you pass in a lambda as the valueChanged argument.
Since the lambda’s definition has a Float parameter, you pass it in, and in this case, I named it value.
Remember, the Slider inside the TargetSlider composable is responsible for emitting this value when the thumb is moved.
Then you set the sliderValue state to this value.
So in a nutshell, when you move the thumb of the slider, the state is updated to the current value the slider emits.
Do note that the value constant is enclosed and is only available in the lambda and this is because it is wrapped in curly braces where it is defined.
Not to worry, you’ll learn about variable scopes in a future episode in this part of the course.
Everything is in sync so its time to try it out.
Run your app.
The slider value is updated as expected when you move the thumb.
With this, it means that the stateless TargetSlider composable receives the state value from where it is called, in this case the GameScreen composable.
And it passes the new value via the valueChanged event.
This process is also known as “Lifting State Up.”
Now, we have the value of the slider stored in a state object, let’s see how to display it in the AlertDialog.