I’m using an IconToggleButton to create a single-choice list, and the variable that tracks whether an item is selected is var choiceChecked: Pair<Int, MutableState>? = null. This variable stores the index of the selected item and its selected state.
When I click a button outside the list and print choiceChecked.first, it prints the correct value. It seems that even though choiceChecked doesn't use remember and MutableState, it can still retain its value after recomposition.
However, when I turn off the screen and turn it back on, the value of choiceChecked is lost. I speculate that the reason is that after selecting an item, choiceChecked recorded the value during that recomposition. Later, when I click the button to print choiceChecked.first, no recomposition occurs because the state hasn't changed, so it prints the correct value.
But when I turn off the screen and turn it back on, recomposition happens, and choiceChecked is lost. Is this correct?
Below is my code,Please forgive me for posting such long code, as I am unsure if other variables might affect recomposition. The code block that prints choiceChecked is commented with 'print the choiceChecked'. The code for selecting the item is inside the clickable. Please search for clickable.
@Composable
fun AlbumGrouping(
showPopup: MutableState<Boolean>,
isPopupVisible: MutableState<Boolean>,
size: DpSize,
application: MediaApplication,
directoryIcon: Bitmap,
) {
val width = remember { (size.width.value * 0.9f).dp }
val height = remember { (size.height.value * 0.6f).dp }
val density = LocalDensity.current
//加载数据
val scope = rememberCoroutineScope()
val items: SnapshotStateList<Album> = remember { mutableStateListOf() }
var queryJob by remember { mutableStateOf<Job?>(null) }
var addJob by remember { mutableStateOf<Job?>(null) }
val gridState = rememberLazyGridState()
//控制显示
val transition = updateTransition(targetState = showPopup.value, label = "popup")
val alpha by transition.animateFloat(
label = "popupOffset",
transitionSpec = { tween(durationMillis = 500) }
) { state -> if (state) 1f else 0f }
// 监听 showPopup 的变化,管理 Popup 显示状态
LaunchedEffect(showPopup.value) {
queryJob?.cancelAndJoin()
if (showPopup.value) {
queryJob = scope.launch(Dispatchers.IO) {
val result = application.mediaDatabase.albumDao.queryByParentId(-1)
if (result != null) items.addAll(result)
}
isPopupVisible.value = true
} else {
delay(500)
isPopupVisible.value = false
queryJob?.cancel()
}
}
var choiceChecked: Pair<Int, MutableState<Boolean>>? = null
if (isPopupVisible.value) {
Popup(
alignment = Alignment.TopCenter,
onDismissRequest = {
showPopup.value = false
},
properties = PopupProperties(focusable = true)
) {
Column(
modifier = Modifier
.size(width, height)
.graphicsLayer(alpha = alpha)
.background(Color(0xE6FFFFFF), shape = RoundedCornerShape(16.dp))
) {
LazyVerticalGrid(
state = gridState,
columns = GridCells.Fixed(3),
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(16.dp)
) {
items(items.size) { index ->
items[index].let { item ->
val checked = remember { mutableStateOf(false) }
Box(
contentAlignment = Alignment.TopEnd,
modifier = Modifier
.padding(end = TinyPadding, top = TinyPadding)
.clickable {
if (checked.value) {
checked.value = false
} else {
choiceChecked?.second?.value = false
choiceChecked = index to checked
checked.value = true
}
}
) {
Column {
DisplayImage(
bitmap = directoryIcon,
context = application.baseContext,
modifier = Modifier.aspectRatio(1f)
)
Text(
text = item.name,
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(start = SmallPadding)
)
}
IconToggleButton(
checked = checked.value,
colors = IconButtonDefaults.iconToggleButtonColors(
containerColor = Color(0x80808080), // 选中时图标的颜色
contentColor = Color.White, // 未选中时图标的颜色
checkedContainerColor = Color.White, // 选中时背景颜色
checkedContentColor = Color.DarkGray // 未选中时背景颜色
),
onCheckedChange = {
if (checked.value) {
checked.value = false
} else {
choiceChecked?.second?.value = false
choiceChecked = index to checked
checked.value = true
}
},
modifier = Modifier
.width(24.dp)
.height(24.dp)
.offset(x = (-10).dp, y = 10.dp)
) {
if (checked.value) {
Icon(
Icons.Filled.CheckCircle,
contentDescription = "选中",
modifier = Modifier
.fillMaxSize()
.padding(0.dp)
)
} else {
Icon(
Icons.Filled.RadioButtonUnchecked,
contentDescription = "未选中",
modifier = Modifier
.fillMaxSize()
.padding(0.dp)
)
}
}
}
}
}
}
val driver = remember { mutableStateOf(0.dp) }
Row(
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
.onGloballyPositioned { layout ->
with(density) {
driver.value = layout.size.height.toDp()
}
}
.background(Color(0xE6D3D3D3), shape = RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp))
) {
TextButton(onClick = { //print the choiceChecked
println("index${choiceChecked?.first}")
}) {
Text(stringResource(R.string.popup_select_grouping))
}
Text(
"", modifier = Modifier
.width(1.dp)
.height(driver.value)
.background(Color(0xE6FFFFFF))
)
TextButton(onClick = {
scope.launch(Dispatchers.IO) {
addJob?.join()
addJob = scope.launch {
val groupingNum = items.size
val album = Album(name = "分组${groupingNum + 1}")
application.mediaDatabase.albumDao.insert(album)
items.add(album)
}
}
}) {
Text(stringResource(R.string.popup_add_grouping))
}
}
}
}
}
}
I made a smaller sample to support my answer:
@Composable
fun RecompositionDemo() {
val context = LocalContext.current
var choiceChecked = 1
Column {
OutlinedButton(
onClick = {
choiceChecked++
}
) {
Text("INCREASE")
}
OutlinedButton(
onClick = {
Toast.makeText(context, "choiceChecked = $choiceChecked", Toast.LENGTH_SHORT).show()
}
) {
Text("PRINT")
}
Text("choiceChecked = $choiceChecked")
}
}
While execution, pay close attention to a) the choiceChecked
value shown by the Toast and b) the choiceChecked
value shown by the Text
Composable:
What we can observe is
Toast
initially displays choiceChecked = 1
OutlinedButton
Text
does not update to show the new valueToast
however will correctly show choiceChecked = 2
choiceChecked = 1
But when I turn off the screen and turn it back on, recomposition happens, and choiceChecked is lost. Is this correct?
This is correct.
When I click a button outside the list and print choiceChecked.first, it prints the correct value. It seems that even though choiceChecked doesn't use remember and MutableState, it can still retain its value after recomposition.
That is not correct. The reason while the Toast
prints the correct choiceChecked
values is because there is no recomposition triggered in the first place when doing choiceChecked++
, and thus the value is not reset. At the same time, as no recomposition is happening, the Text
Composable is not updating.
Jetpack Compose can only detect changes on MutableState
variables, that's why we use mutableStateOf
to make sure the UI gets recomposed when a variable value changes. Additionally, we use remember
so that any new value set to a variable survives the recomposition.