Jetpack Compose LazyColumn recomposition with remember() - android

Ive been trying out Jetpack Compose and ran into something with the LazyColumn list and remember().
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp{
MyScreen()
}
}
}
}
#Composable
fun MyApp(content: #Composable () -> Unit){
ComposeTestTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
content()
}
}
}
#Composable
fun MyScreen( names: List<String> = List(1000) {"Poofy #$it"}) {
NameList( names, Modifier.fillMaxHeight())
}
#Composable
fun NameList( names: List<String>, modifier: Modifier = Modifier ){
LazyColumn( modifier = modifier ){
items( items = names ) { name ->
val counter = remember{ mutableStateOf(0) }
Row(){
Text(text = "Hello $name")
Counter(
count = counter.value,
updateCount = { newCount -> counter.value = newCount } )
}
Divider(color = Color.Black)
}
}
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button( onClick = {updateCount(count+1)} ){
Text("Clicked $count times")
}
}
This runs and creates a list of 1000 rows where each row says "Hello Poofy #N" followed by a button that says "Clicked N times".
It all works fine but if I click a button to update its count that count will not persist when it is scrolled offscreen and back on.
The LazyColumn "recycling" recomposes the row and the count. In the above sample the counter is hoisted up into NameList() but I have tried it unhoisted in Counter(). Neither works.
What is the proper way to remember the count? Must I store it in an array in the activity or something?

The representations for items are recycled, and with the new index the value of remember is reset. This is expected behavior, and you should not expect this value to persist.
You don't need to keep it in the activity, you just need to move it out of the LazyColumn. For example, you can store it in a mutable state list, as shown here:
val counters = remember { names.map { 0 }.toMutableStateList() }
LazyColumn( modifier = modifier ){
itemsIndexed(items = names) { i, name ->
Row(){
Text(text = "Hello $name")
Counter(
count = counters[i],
updateCount = { newCount -> counters[i] = newCount } )
}
Divider(color = Color.Black)
}
}
Or in a mutable state map:
val counters = remember { mutableStateMapOf<Int, Int>() }
LazyColumn( modifier = modifier ){
itemsIndexed(items = names) { i, name ->
Row(){
Text(text = "Hello $name")
Counter(
count = counters[i] ?: 0,
updateCount = { newCount -> counters[i] = newCount } )
}
Divider(color = Color.Black)
}
}
Note that remember will also be reset when screen rotates, consider using rememberSaveable instead of storing the data inside a view model.
Read more about state in Compose in documentation

Related

Use of State hosting to change variable in jetpack compose

I want to change the value of variable in jetpack compose. I am trying to use Stateful and Stateless with some code, but it have some problem to increment the value. Can you guys guide me on this.
ItemColorStateful
#Composable
fun ItemColorStateful() {
var index by remember { mutableStateOf(-1) }
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "Different Color")
ButtonScopeStateless(
index = { index },
onIndexChange = {
index = it
}
)
}
}
ButtonScopeStateless
#Composable
fun ButtonScopeStateless(
index: () -> Int,
onIndexChange: (Int) -> Unit,
) {
Button(onClick = { onIndexChange(index()++) }) {
Text(text = "Click Me $index")
}
}
I am getting error on index()++.
Using the general pattern for state hoisting your stateless composable should have two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
In your case:
index: Int,
onIndexChange: (Int) -> Unit
Also you should respect the Encapsulated properties: Only stateful composables can modify their state. It's completely internal.
Use onIndexChange(index+1) instead of onIndexChange(index()++). In this way the state is modified by the ItemColorStateful.
You can use:
#Composable
fun ItemColorStateful() {
var index by remember { mutableStateOf(-1) }
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "Different Color")
ButtonScopeStateless(
index = index ,
onIndexChange = {
index = it
}
)
}
}
#Composable
fun ButtonScopeStateless(
index: Int, //value=the current value to display
onIndexChange: (Int) -> Unit //an event that requests the value to change, where Int is the proposed new value
) {
Button(onClick = { onIndexChange(index+1) }) {
Text(text = "Click Me $index")
}
}
ItemColorStateful
#Composable
fun ItemColorStateful() {
var index by remember { mutableStateOf(-1) }
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "Different Color")
ButtonScopeStateless(
index = index ,
onIndexChange = {
index++
}
)
}
}
ButtonScopeStateless
#Composable
fun ButtonScopeStateless(
index: Int,
onIndexChange: () -> Unit,
) {
Button(onClick = {
onIndexChange()
}) {
Text(text = "Click Me $index")
}
}

Avoid Recomposition in nested column in jetpack compose

I am working on nested column in jetpack compose. I have one list which is huge amount of data coming from server. I was checked in Layout Inspector and I see that whenever my item is added in list it recompose and increase counts. So my doubt is if I add 100 item in list one by one, so my Nested Column will be 100 times recompose ? If not can someone help me on this please?
ListViewComposableActivity.kt
class ListViewComposableActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppBarScaffold(
displayHomeAsUpEnabled = true,
titleId = R.string.activity
) {
ListViewItemStateful()
}
}
}
}
ListViewItemStateful
#Composable
fun ListViewItemStateful(
viewModel: ListViewModel = koinViewModel(),
) {
ItemViewListStateless(
uiState = viewModel.uiState,
isEnable = viewModel.isEnable,
scanDeviceList = viewModel.scanResultList,
)
}
ItemViewListStateless
#Composable
fun ItemViewListStateless(
uiState: State,
isEnable: Boolean,
scanDeviceList: SnapshotStateList<ScanResults>,
) {
when (uiState) {
INITIAL,
FIRST -> {
ListContent(isEnable, scanDeviceList)
}
}
}
ListContent
#Composable
fun ListContent(isEnable: Boolean, scanDeviceList: SnapshotStateList<ScanResults>) {
AnimatedVisibility(true) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
if (isEnable) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
DeviceList(
scanDeviceList,
modifier = Modifier.align(Alignment.Start),
)
}
}
}
}
}
DeviceList
#Composable
fun ColumnScope.DeviceList(
scanDeviceList: SnapshotStateList<ScanResults>,
modifier: Modifier = Modifier,
) {
Spacer(modifier = Modifier.height(32.dp))
AnimatedVisibility(
scanDeviceList.isNotEmpty(),
modifier = modifier
) {
Column {
Text(text = "Device List")
scanDeviceList.forEachIndexed { index, scanResults ->
Text(text = scanResults.device.name)
}
}
}
}
ListViewModel.kt
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope
import com.abc.app.common.BaseViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class ListViewModel : BaseViewModel() {
val scanResultList by lazy { mutableStateListOf<ScanResults>() }
var isEnable by mutableStateOf(false)
private set
var uiState by mutableStateOf<State>(State.INITIAL)
private set
init {
viewModelScope.launch {
(0..10).forEach {
delay(2000)
scanResultList.add(ScanResults(Device("item $it")))
}
}
isEnable = true
uiState = State.FIRST
}
}
data class ScanResults(val device: Device)
data class Device(val name: String)
enum class State {
INITIAL,
FIRST
}
I am adding few items in list to show in layout inspector
In above image you can see the DeviceList is recompose 10 times.
I checked in Jetpack Compose: Debugging recomposition around 6:40 min he tried to solve recompose issue and there skipped recomposition count is clear. So why it's showing count in my component tree in recomposition and skipped section? Many thanks
UPDATE
When I was changed to #Thracian answer it still recomposition skip
#Composable
fun ColumnScope.DeviceList(
scanDeviceList:()-> SnapshotStateList<ScanResults>,
modifier: Modifier = Modifier,
) {
Spacer(modifier = Modifier.height(32.dp))
AnimatedVisibility(
scanDeviceList().isNotEmpty(),
modifier = modifier
) {
Column {
Text(text = "Device List")
scanDeviceList().forEachIndexed { index, scanResults ->
Item(scanResults.device)
}
}
}
}
#Composable
private fun Item(device: Device) {
Text(
modifier = Modifier.border(2.dp, getRandomColor()),
text = device.name
)
}
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
In your question when you add a new item to SnapshotStateList whole Column is composed because Column doesn't create a composition scope due to inline keyword. If you create a scope that scope is recomposed when the value it reads changes. You can refer this question and answer as well.
Jetpack Compose Smart Recomposition
Add an item where Text reads device
#Composable
private fun Item(device: Device) {
Text(
modifier = Modifier.border(2.dp, getRandomColor()),
text = device.name
)
}
Random color is something i use for displaying recomposition visually
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
Your current setup
With Item Composable that creates scope.
#Composable
fun ColumnScope.DeviceList(
scanDeviceList: SnapshotStateList<ScanResults>,
modifier: Modifier = Modifier,
) {
Spacer(modifier = Modifier.height(32.dp))
AnimatedVisibility(
scanDeviceList.isNotEmpty(),
modifier = modifier
) {
Column {
Text(text = "Device List", color = getRandomColor())
scanDeviceList.forEachIndexed { index, scanResults ->
// Text(
// modifier = Modifier.border(2.dp, getRandomColor()),
// text = scanResults.device.name
// )
Item(scanResults.device)
}
}
}
}
And when you have many items, especially they don't fit viewport you can use LazyColumn instead of Column with verticalScroll to limit recomposition amount to number of items that are visible on viewport or visible area of LazyColumn

Change background color of surface item when click in jetpack compose

I want to make a background color change when user select a item on Surface. I am using Column in my parent, so I can't make LazyColumn. So I am using Foreach to make a list of view. By default no view will be selected, when user click on item then I want change the color. Note: Only one item will select at a time.
ScanDeviceList
#Composable
fun ColumnScope.ScanDeviceList(
scanDeviceList: List<ScanResult>,
modifier: Modifier = Modifier,
pairSelectedDevice: () -> Unit
) {
Spacer()
AnimatedVisibility() {
Column {
Text()
Spacer()
scanDeviceList.forEachIndexed { index, scanResult ->
ClickableItemContainer(
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){}
}
AvailableWarningText()
PairSelectedDevice(pairSelectedDevice)
}
}
}
ScanDeviceItem
#Composable
fun ScanDeviceItem(
index: Int,
scanResult: ScanResult,
scanDeviceList: List<ScanResult>
) {
Column {
if (index == 0) {
Divider(color = Cloudy, thickness = 1.dp)
}
Text(
text = scanResult.device.name,
modifier = Modifier.padding(vertical = 10.dp)
)
if (index <= scanDeviceList.lastIndex) {
Divider(color = Cloudy, thickness = 1.dp)
}
}
}
ClickableItemContainer
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ClickableItemContainer(
rippleColor: Color = TealLight,
content: #Composable (MutableInteractionSource) -> Unit,
clickAction: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
CompositionLocalProvider(
LocalRippleTheme provides AbcRippleTheme(rippleColor),
content = {
Surface(
onClick = { clickAction() },
interactionSource = interactionSource,
color = White
) {
content(interactionSource)
}
}
)
}
I want to something like this
Now above solution is only working on ripple effect, Now I want to extend my function to select a single item at a time. Many Thanks
You can store the selected index and use the clickAction to update its value when the user clicks on each item.
Something like:
#Composable
fun MyList(
var selectedIndex by remember { mutableStateOf(-1) }
Column() {
itemsList.forEachIndexed() { index, item ->
MyItem(
selected = selectedIndex == index,
clickAction = { selectedIndex = index }
)
}
}
#Composable
fun MyItem(
selected : Boolean = false,
clickAction: () -> Unit
){
Surface(
onClick = { clickAction() },
color = if (selected) Color.Red else Color.Yellow
) {
Text("Item...")
}
}
To have only item selected at a time, you can follow a behavior that is similar. You save your item selected in a mutableState variable, like:
var itemSelected by remember { mutableState(ScanDeviceResult()) }
After that, when you click the item, you update the itemSelected, something like:
ClickableItemContainer(
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){ newItemSelected ->
itemSelected = newItemSelected
}
Now, each ClickableItemContainer needs to have an associated ScanResult.
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ClickableItemContainer(
itemSelected: ScanResult,
scanResult: ScanResult,
rippleColor: Color = TealLight,
content: #Composable (MutableInteractionSource) -> Unit,
clickAction: (ScanResult) -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
CompositionLocalProvider(
LocalRippleTheme provides AbcRippleTheme(rippleColor),
content = {
Surface(
onClick = { clickAction.invoke(scanResult) },
interactionSource = interactionSource,
color = if (itemSelected == scanResult) YOUR_BACKGROUND-SELECTED_COLOR else White
) {
content(interactionSource)
}
}
)
}
Now, on your ScanDeviceList method:
#Composable
fun ColumnScope.ScanDeviceList(
itemSelected: MutableState<ScanResult>,
scanDeviceList: List<ScanResult>,
modifier: Modifier = Modifier,
pairSelectedDevice: () -> Unit
) {
Spacer()
AnimatedVisibility() {
Column {
Text()
Spacer()
scanDeviceList.forEachIndexed { index, scanResult ->
ClickableItemContainer(
itemSelected = itemSelected,
scanResult = scanResult,
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){ scanResultToSelect ->
itemSelected = scanResultToSelect
}
}
AvailableWarningText()
PairSelectedDevice(pairSelectedDevice)
}
}
}

How to use sealed class data to display alphabet index scroller

After creating a sealed class for my LazyColumn, how can I use the inital of every item for an alphabet scroller? it.? is where my problem occurs as for some reason, it does not let me accesss my sealed class and use it, i.e. itemName.
sealed class Clothes {
data class FixedSizeClothing(val itemName: Int, val sizePlaceholder: Int): Clothes()
data class MultiSizeClothing(val itemName: Int, val sizePlaceholders:
List<Int>): Clothes()
}
val clothingItems = remember { listOf(
Clothes.FixedSizeClothing(itemName = R.string.jumper, itemPlaceholder = 8),
Clothes.MultiSizeClothing(itemName = R.string.dress, itemPlaceholders = listOf(0, 2))
)
}
val headers = remember { clothingItems.map { getString(it.?).first().uppercase() }.toSet().toList() }
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier
.weight(1f)
.padding(it)
) {
items(clothingItems) {
val text1 = when (it) {
is Clothes.FixedSizeClothing ->
stringResource(id = it.itemName)
is Clothes.MultiSizeClothing ->
stringResource(id = it.itemName)
}
val text2 = when (it) {
is Clothes.FixedSizeClothing ->
stringResource(id = R.string.size_placeholder, it.sizePlaceholder)
is Clothes.MultiSizeClothing ->
stringResource(id = R.string.size_placeholder_and_placeholder, it.itemPlaceholders[0], it.itemPlaceholders[1])
}
Column(modifier = Modifier
.fillMaxWidth()
.clickable {}) {...}
}
}
val offsets = remember { mutableStateMapOf<Int, Float>() }
var selectedHeaderIndex by remember { mutableStateOf(0) }
val scope = rememberCoroutineScope()
fun updateSelectedIndexIfNeeded(offset: Float) {
val index = offsets
.mapValues { abs(it.value - offset) }
.entries
.minByOrNull { it.value }
?.key ?: return
if (selectedHeaderIndex == index) return
selectedHeaderIndex = index
val selectedItemIndex = clothingItems.indexOfFirst { getString(it.?).first().uppercase() == headers[selectedHeaderIndex] }
scope.launch {
listState.scrollToItem(selectedItemIndex)
}
}
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxHeight()
.background(Color.Gray)
.pointerInput(Unit) {
detectTapGestures {
updateSelectedIndexIfNeeded(it.y)
}
}
.pointerInput(Unit) {
detectVerticalDragGestures { change, _ ->
updateSelectedIndexIfNeeded(change.position.y)
}
}
) {
headers.forEachIndexed { i, header ->
Text(
header,
modifier = Modifier
.onGloballyPositioned {
offsets[i] = it.boundsInParent().center.y
},
color = Color.White
)
}
}
You can define the val itemName: Int in the parent Clothes class and override it in you other subclasses. If you do that, you then do not need to use a when expression if you just want to access the itemName property.
The parent class can be a sealed interface instead of a sealed class. That way it is a bit more flexible and a bit less verbose when overriding its properties. Also : Clothes() then becomes just : Clothes
sealed interface Clothes {
val itemName: Int
data class FixedSizeClothing(override val itemName: Int, val sizePlaceholder: Int): Clothes
data class MultiSizeClothing(override val itemName: Int, val sizePlaceholders: List<Int>): Clothes
}
And the line where you create the headers becomes
val headers = clothingItems.map { stringResource(it.itemName).first().uppercase() }.toSet().toList()
The remember {} does not make much sense because if your data changes so can the set of initial letters. You also cannot use the stringResouce() function inside remember {}, because stringResouce() has to be used inside a #Composable function.
There is a different way of obtaining Resources and then using resources.getString(...) if you would like to retrieve resource strings inside a remember {} block. But in this case the remember {} block does not make sense due to the data potentially changing. The optimization to cache initial letters would have to be done in a different way.

State hoisting for each item in LazyColumn

I've gone through this codelab. In step number 7, when clicking on single row's text it's changing its color, but function will not keep track of it, meaning it will disappear after re-composition.
I want list to remember color of single item thus I've move state hoisting to the NameList function level.
Unfortunately it's not working.
Where's the bug?
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(items = names) { name, ->
val isSelected = remember { mutableStateOf(false)}
Greeting(name = name,isSelected.value){ newSelected -> isSelected.value = newSelected}
Divider(color = Color.Black)
}
}
}
#Composable
fun Greeting(name: String,isSelected : Boolean, updateSelected : (Boolean) -> Unit) {
val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
Text(
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = { updateSelected(!isSelected)}),
text = "Hello $name",
)
}
You should hoist your selection state to the caller of NameList function.
#Composable
fun MyScreen() {
// Fake list of names
val namesList = (1..100).map { "Item $it" }
// Here, we're keeping the selected positions.
// At the beginning, all names are not selected.
val selection = remember {
mutableStateListOf(*namesList.map { false }.toTypedArray())
}
NameList(
// list of names
names = namesList,
// list of selected items
selectedItems = selection,
// this function will update the list above
onSelected = { index, selected -> selection[index] = selected },
// just to occupy the whole screen
modifier = Modifier.fillMaxSize()
)
}
Then, your NameList will look like this:
#Composable
fun NameList(
names: List<String>,
selectedItems: List<Boolean>,
onSelected: (index: Int, selected: Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
itemsIndexed(items = names) { index, name ->
Greeting(
name = name,
isSelected = selectedItems[index],
updateSelected = { onSelected(index, it) }
)
Divider(color = Color.Black)
}
}
}
Nothing changes on Greeting function.
Here is the result:

Categories

Resources