I am new in Compose, and I am struggling with the state issue.
Let's consider such scenario:
The list of items is displayed.
On the bottom of the screen there is a button which state should change if user reaches the bottom of the list.
When button's state is changed, its text content and sharp is changed.
When the state is changed and user scrolls up -> state should not be changed anymore.
It works when I scroll to the bottom -> the state is changed.
But state is also changed when I scroll up from the bottom of the list.
val scrollState = rememberLazyListState()
val lastVisibleItemIndex by remember(scrollState) {
derivedStateOf {
scrollState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
}
}
val isEndReached by remember { lastVisibleItemIndex == items.size -1 }
This should do the trick
var isEndReached by remember { mutableStateOf(false) }
if (lastVisibleItemIndex == items.size - 1) {
isEndReached = true
}
Related
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.
I have a VerticalPager inside HorizontalPager.
When I scroll VerticalPager down to the Nth page in the 1st page of the HorizontalPager, then scroll to other pages in HorizontalPager, then come back to the 1st page of the HorizontalPager the Nth page I scrolled down to in VerticalPager is saved.
I want always the 1st page of the VerticalPager (not the Nth page I scolled to) to be open whenever HorizontalPager is scrolled.
How can I achieve that?
My code:
val pagerState = rememberPagerState()
HorizontalPager(count = myList.size, state = pagerState) {
idx ->
myList[idx].let { cur ->
val verPagerState = rememberPagerState(initialPage = 0)
VerticalPager(
count = cur.photos.size,
state = verPagerState
) { page ->
}
}
}
Here's how you can scroll it manually when the page is no longer visible.
I use snapshotFlow, which creates a flow that emit values when the state used inside changes.
val verPagerState = rememberPagerState(initialPage = 0)
LaunchedEffect(Unit) {
snapshotFlow {
!pagerState.isScrollInProgress
&& pagerState.currentPage != idx
&& verPagerState.currentPage != 0
}.filter { it }
.collect {
verPagerState.scrollToPage(0)
}
}
What I am trying to achieve is if there are two horizontal pagers, then on swiping top one to left then the bottom horizontal pager should swipe to right and vice-versa, have tried using pagerState scrollBy method but not getting desired output
First of all, you need to determine which pager is scrolled and which one should follow it. This can be done with isScrollInProgress, I use it inside derivedStateOf to avoid unnecessary re-compositions.
Then I run LaunchedEffect. I'm passing pair of pager states in needed order, or null, as a key, so LaunchedEffect will be re-launched when scrolling stops/starts. Using snapshotFlow, it is possible to track changes in the result of a block whose calculations depend on state changes.
PagerState has scroll position information in the properties currentPage and currentPageOffset. scrollToPage takes only 0..1 values for page offset, but currentPageOffset can be less than zero when scrolling backward.
Suppose currentPage = 2 and currentPageOffset = -0.1. In this case, I will get 1.9 in pagePart, and I need to split it back to get 1 and 0.9. To do this I use divideAndRemainder: it will return a list of the form listOf(1.0, 0.9).
Column {
val count = 10
val firstPagerState = rememberPagerState()
val secondPagerState = rememberPagerState()
val scrollingFollowingPair by remember {
derivedStateOf {
if (firstPagerState.isScrollInProgress) {
firstPagerState to secondPagerState
} else if (secondPagerState.isScrollInProgress) {
secondPagerState to firstPagerState
} else null
}
}
LaunchedEffect(scrollingFollowingPair) {
val (scrollingState, followingState) = scrollingFollowingPair ?: return#LaunchedEffect
snapshotFlow { scrollingState.currentPage + scrollingState.currentPageOffset }
.collect { pagePart ->
val divideAndRemainder = BigDecimal.valueOf(pagePart.toDouble())
.divideAndRemainder(BigDecimal.ONE)
followingState.scrollToPage(
divideAndRemainder[0].toInt(),
divideAndRemainder[1].toFloat(),
)
}
}
HorizontalPager(
count = count,
state = firstPagerState,
modifier = Modifier.weight(1f)
) {
Text(it.toString())
}
HorizontalPager(
count = count,
state = secondPagerState,
modifier = Modifier.weight(1f)
) {
Text(it.toString())
}
}
Result:
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
}
}