Reputation: 51
In a Jetpack Compose application, I have two composables similar to here:
@Composable
fun Main() {
println("Composed Main")
val context = LocalContext.current
var text by remember { mutableStateOf("") }
fun update(num: Number) {
text = num.toString()
Toast.makeText(context, "Toast", Toast.LENGTH_SHORT).show()
}
Column {
Text(text)
Keypad { update(it) }
}
}
@Composable
fun Keypad(onClick: (Number) -> Unit) {
println("Composed Keypad")
Column {
for (i in 1..10) {
Button(onClick = {onClick(i)}) {
Text(i.toString())
}
}
}
}
Clicking each button causes the two composables to recompose and produces this output:
I/System.out: Composed Main
I/System.out: Composed Keypad
Recomposing the Keypad
composable is unneeded and makes the app freeze (for several seconds in a bigger project).
Removing usages of context in the event handles (in here, commenting out the Toast
) solves the problem and does not recompose the Keypad
and produces this output:
I/System.out: Composed Main
Is there any other way I could use context in an event without causing unneeded recompositions?
Upvotes: 4
Views: 1832
Reputation: 6257
The issue is the Context
not being a stable (@Stable
) type. The lambda/callback of KeyPad
is updating a state and its immediately followed by a component that uses an unstable Context
, this results to the onClickLambda
to be re-created (you can see its hashcode changing everytime you click a button), thus making the Keypad
composable not skippable.
You can consider four approaches to deal with your issue. I also made some changes to your code removing the local function and put everything directly in the lambda/callback to make everything smaller.
For the first two, start first by creating a generic wrapper class like this.
@Stable
data class StableWrapper<T>(val value: T)
Using the generic wrapper class, you can consider wrapping the context and use it like this
@Composable
fun Main() {
Log.e("Composable", "Composed Main")
var text by remember { mutableStateOf("") }
val context = LocalContext.current
val contextStableWrapper = StableWrapper(context)
Column {
Text(text)
Keypad {
text = it.toString()
Toast.makeText(contextStableWrapper.value, "Toast", Toast.LENGTH_SHORT).show()
}
}
}
Toast is also an unstable type, so you have to make it "stable" with this second approach.
Note that this only applies if your Toast message will not change.
Hoist them up above your Main
where you'll create an instance of your static-message Toast and put it inside the stable wrapper
val toastWrapper = StableWrapper(
Toast.makeText(LocalContext.current, "Toast", Toast.LENGTH_SHORT)
)
Main(toastWrapper = toastWrapper)
and your Main composable will look like this
@Composable
fun Main(toastWrapper: StableWrapper<Toast>) {
Log.e("Composable", "Composed Main")
var text by remember { mutableStateOf("") }
Column {
Text(text)
Keypad {
text = it.toString()
toastWrapper.value.show()
}
}
}
remember{…}
the Context(I might expect some correction here), I think this is called "memoizing the value (Context
) inside remember{…}
", this looks similar to a deferred read.
@Composable
fun Main() {
Log.e("Composable", "Composed Main")
var text by remember { mutableStateOf("") }
val context = LocalContext.current
val rememberedContext = remember { { context } }
Column {
Text(text)
Keypad {
text = it.toString()
Toast.makeText(rememberedContext(), "Toast", Toast.LENGTH_SHORT).show()
}
}
}
You can utilize Compose Side-Effects and put the Toast in them.
Here, SideEffect
will execute every post-recomposition.
SideEffect {
if (text.isNotEmpty()) {
Toast.makeText(context, "Toast", Toast.LENGTH_SHORT).show()
}
}
or you can utilize LaunchedEffect
using the text
as its key, so on succeeding re-compositions, when the text
changes, different from its previous value (invalidates), the LaunchedEffect
will re-execute and show the toast again
LaunchedEffect(key1 = text) {
if (text.isNotEmpty()) {
Toast.makeText(context, "Toast", Toast.LENGTH_SHORT).show()
}
}
Replacing your print with Log statements, this is the output of any of the approaches when clicking the buttons
E/Composable: Composed Main // first launch of screen
E/Composable: Composed Keypad // first launch of screen
// succeeding clicks
E/Composable: Composed Main
E/Composable: Composed Main
E/Composable: Composed Main
E/Composable: Composed Main
The only part I'm still not sure of is the first approach, even if Toast is not a stable type based on the second, just wrapping the context in the stable wrapper in the first approach is sufficient enough for the Keypad
composable to get skipped.
Upvotes: 3