I have created a LazyRow in jetpack compose. At a certain point in the viewmodel, I want to get the list of currently visible items from that LazyRow.
I know that I can get the list of visible items in the Composable function using the following code:
val listState = rememberLazyListState()
val visibleItemIds = remember {
derivedStateOf { listState.layoutInfo.visibleItemsInfo.map { it.key.toString() } }
}
The problem is how can I pass this data to the viewmodel during a viewmodel event (not a button click etc)
You can add a side effect to know what are the visible items in any time.
LaunchedEffect(visibleItemIds){
//update your viewModel
}
You can also have a List<T> instead of State<List<String>> as in your code with:
val state = rememberLazyListState()
val visibleItemIds: List<Int> by remember {
derivedStateOf {
val layoutInfo = state.layoutInfo
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) {
emptyList()
} else {
visibleItemsInfo.map { it.index }
}
}
}
Also note that visibleItemsInfo returns also the partially visible items.
Related
I need help with composables and mutableStates!
In the example below, I have a list of classes.
I want to try to manipulate the list to
a. swap the position of two items
b. change the "data" in one of the classes
c. get a copy of the classes "flattened"
I have in the main composable a text field displaying the items
a. a copy of the list from the viewModel
b. a button to move the 1st item to the 3rd position
c. a button to get the "flattened" data
d. a button to modify the data in the 3rd item.
I expected each of these actions to modify the visible contents
of the items.
When I run this I see the items on the screen.
When I click "move 2nd to 4th", the list of items does not update on the screen.
But, clicking to get the flattened layout shows the flattened data correctly.
Now, I click "change data" and the data is changed in the items on the screen!
If I click on get flat layout, the original list reappears and the
flattened list shows the original rlattened list again!
What is happening and how can I fix it? I don't expect coroutine calls are
needed here.
I also have the problem of losing data when I rotate the screen (newbie here.)
Pertinent code of MainActivity:
#Composable
fun Screen() {
val vm = MyViewModel()
val items = vm.items.collectAsState()
var flat by remember{ mutableStateOf ("")}
var newItem by remember { mutableStateOf(MyClass("",""))}
Column {
Text(text = items.toString().split("MyC").joinToString("\nMyC"))
Spacer(Modifier.height(8.dp))
Button(
onClick = {vm.moveList(1,3)} // swap 2nd and 4th items on list
) { Text( text = "move 2nd to 4th")}
Spacer(Modifier.height(8.dp))
Button(
onClick = {flat = vm.getLayout()}
) { Text( text = "get flat layout")}
Spacer(Modifier.height(8.dp))
Text(text = flat)
Spacer(Modifier.height(8.dp))
Button(
onClick = { vm.changeData(2,"NEW")
newItem = vm.items.value[2]}
) { Text( text = "get change heading on 3rd")}
Spacer(Modifier.height(8.dp))
Text(text = newItem.toString())
}
}
And here is code from the viewModel:
data class MyClass(
val key: String,
val data: String,
)
class MyViewModel: ViewModel(){
companion object {
private const val DEFAULT_LAYOUT = "0|A,1|B,2|C,3|D,4|E"
}
private val _items: MutableStateFlow<List<MyClass>> =
MutableStateFlow(
DEFAULT_LAYOUT.split(",").map{ MyClass(it.split("|").first(),it.split("|").last())})
val items: StateFlow<List<MyClass>> = _items
fun getLayout(): String {
return items.value.map { it.key + "|" + it.data }.joinToString(",")
}
fun changeData(
index: Int,
newData: String
) {
val mC : MyClass = items.value[index].copy(data = newData)
_items.update {
it.toMutableList().apply {
set(index,mC)
}
}
}
fun moveList(from: Int, to: Int) {
_items.update {
it.toMutableList().apply {
add(to, removeAt(from))
}
}
}
fun removeElement(idx: Int) {
_items.update {
it.toMutableList().apply {
removeAt(idx)
}
}
}
}
For example, I load data into a List, it`s wrapped by MutableStateFlow, and I collect these as State in UI Component.
The trouble is, when I change an item in the MutableStateFlow<List>, such as modifying attribute, but don`t add or delete, the UI will not change.
So how can I change the UI when I modify an item of the MutableStateFlow?
These are codes:
ViewModel:
data class TestBean(val id: Int, var name: String)
class VM: ViewModel() {
val testList = MutableStateFlow<List<TestBean>>(emptyList())
fun createTestData() {
val result = mutableListOf<TestBean>()
(0 .. 10).forEach {
result.add(TestBean(it, it.toString()))
}
testList.value = result
}
fun changeTestData(index: Int) {
// first way to change data
testList.value[index].name = System.currentTimeMillis().toString()
// second way to change data
val p = testList.value[index]
p.name = System.currentTimeMillis().toString()
val tmplist = testList.value.toMutableList()
tmplist[index].name = p.name
testList.update { tmplist }
}
}
UI:
setContent {
LaunchedEffect(key1 = Unit) {
vm.createTestData()
}
Column {
vm.testList.collectAsState().value.forEachIndexed { index, it ->
Text(text = it.name, modifier = Modifier.padding(16.dp).clickable {
vm.changeTestData(index)
Log.d("TAG", "click: ${index}")
})
}
}
}
Both Flow and Compose mutable state cannot track changes made inside of containing objects.
But you can replace an object with an updated object. data class is a nice tool to be used, which will provide you all copy out of the box, but you should emit using var and only use val for your fields to avoid mistakes.
Check out Why is immutability important in functional programming?
testList.value[index] = testList.value[index].copy(name = System.currentTimeMillis().toString())
I am using a LazyColumn and there are several items in which one of item has a LaunchedEffect which needs to be executed only when the view is visible.
On the other hand, it gets executed as soon as the LazyColumn is rendered.
How to check whether the item is visible and only then execute the LaunchedEffect?
LazyColumn() {
item {Composable1()}
item {Composable2()}
item {Composable3()}
.
.
.
.
item {Composable19()}
item {Composable20()}
}
Lets assume that Composable19() has a Pager implementation and I want to start auto scrolling once the view is visible by using the LaunchedEffect in this way. The auto scroll is happening even though the view is not visible.
LaunchedEffect(pagerState.currentPage) {
//auto scroll logic
}
LazyScrollState has the firstVisibleItemIndex property. The last visible item can be determined by:
val lastIndex: Int? = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
Then you test to see if the list item index you are interested is within the range. For example if you want your effect to launch when list item 5 becomes visible:
val lastIndex: Int = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
LaunchedEffect((lazyListState.firstVisibleItemIndex > 5 ) && ( 5 < lastIndex)) {
Log.i("First visible item", lazyListState.firstVisibleItemIndex.toString())
// Launch your auto scrolling here...
}
LazyColumn(state = lazyListState) {
}
NOTE: For this to work, DON'T use rememberLazyListState. Instead, create an instance of LazyListState in your viewmodel and pass it to your composable.
If you want to know if an item is visible you can use the LazyListState#layoutInfo that contains information about the visible items.
Since you are reading the state you should use derivedStateOf to avoid redundant recompositions and poor performance
To know if the LazyColumn contains an item you can use:
#Composable
private fun LazyListState.containItem(index:Int): Boolean {
return remember(this) {
derivedStateOf {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (layoutInfo.totalItemsCount == 0) {
false
} else {
visibleItemsInfo.toMutableList().map { it.index }.contains(index)
}
}
}.value
}
Then you can use:
val state = rememberLazyListState()
LazyColumn(state = state){
//items
}
//Check for a specific item
var isItem2Visible = state.containItem(index = 2)
LaunchedEffect( isItem2Visible){
if (isItem2Visible)
//... item visible do something
else
//... item not visible do something
}
If you want to know all the visible items you can use something similar:
#Composable
private fun LazyListState.visibleItems(): List<Int> {
return remember(this) {
derivedStateOf {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (layoutInfo.totalItemsCount == 0) {
emptyList()
} else {
visibleItemsInfo.toMutableList().map { it.index }
}
}
}.value
}
I am aware of the remember lazy list state and it works fine
setContent {
Test(myList) // Call Test with a dummy list
}
#Composable
fun Test(data: List<Int>){
val state = rememberLazyListState()
LazyColumn(state = state) {
items(data){ item ->Text("$item")}
}
}
It will remember scroll position and after every rotation and change configuration it will be the same
But whenever I try to catch data from database and use some method like collectAsState
it doesn't work and it seem an issue
setContent{
val myList by viewModel.getList.collectAsState(initial = listOf())
Test(myList)
}
Unfortunately for now there's not a native way to do so, but you can use this code:
val listState = rememberLazyListState()
listState has 3 methods:
firstVisibleItemIndex
firstVisibleItemScrollOffset
isScrollInProgress
All of them are State() so you will always get the data as it updates. For example, if you start scrolling the list, isScrollInProgress will change from false to true.
SAVE AND RESTORE STATE
val listState: LazyListState = rememberLazyListState(viewModel.index, viewModel.offset)
LaunchedEffect(key1 = listState.isScrollInProgress) {
if (!listState.isScrollInProgress) {
viewModel.index = listState.firstVisibleItemIndex
viewModel.offset = listState.firstVisibleItemScrollOffset
}
}
My problem is that live data observer is triggered Observer<T> { state.value = it } with the correct data but compose doesn't kick on recompose. Only when I add an item all changes are propagated. There must some checking on the list itself if it has changed. I guess it doens't compare list items.
#Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
val lifecycleOwner = LifecycleOwnerAmbient.current
val state = remember { mutableStateOf(initial) }
onCommit(this, lifecycleOwner) {
val observer = Observer<T> { state.value = it }
observe(lifecycleOwner, observer)
onDispose { removeObserver(observer) }
}
return state
}
val items: List<TrackedActivityWithMetric> by vm.activities.observeAsState(mutableListOf())
LazyColumnForIndexed(
items = items,
Modifier.padding(8.dp)
) { index, item ->
....
MetricBlock(item.past[1], item.activity.id )
}
So behind the scenes there must be some kind hash comparing mechanism preventing rendering same item twice (More elabored answer wanted). The incorrect rendering was caused by property which was not in TrackedActivityWithMetric data class constructor.
Jetpack Compose does not work well with MutableList, you need to use a List and do something like this:
var myList: List<MyItem> by mutableStateOf(listOf())
private set
for adding an item:
fun addItem(item: MyItem) {
myList = myList + listOf(myItem)
}
for editing an item:
fun editItem(item: MyItem) {
val index = myList.indexOf(myItem)
myList = myList.toMutableList().also {
it[index] = myItem
}
}