Compose - Scroll VerticalPager to the top when outer HorizontalPager is scrolled - android

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)
}
}

Related

Android jetpack compose pagination : Pagination not working with staggered layout jetpack compose

Pagination given by android (https://developer.android.com/topic/libraries/architecture/paging/v3-overview) is working fine with Column,Row,lazy column, lazy rows. Problem occurs when I am trying to achieve pagination in staggered layout (Answer How to achieve a staggered grid layout using Jetpack compose? was very helpful).
Problem statement is there is no further network call when I scroll towards bottom of the list. As per docs there is no method for making paginated calls for next items it just automatically does as soon as we make input list as itemList.collectAsLazyPagingItems() and pass it to lazycolumn/lazyrow. But its not automatically happening for above mentioned staggered layout.
One solution I am testing is there is manual observation on the index of visible items and if they are near the end of the list and manually calling the network request. (see start code for this code lab ( https://developer.android.com/codelabs/android-paging#0 )
Staggered layout somehow in an essence of implementation of creating and using multiple COLUMNS inside and distributing items to them columns. Challenge here is how do we know we are approaching towards the end of the list.
Code for staggered layout is something like this (tbh i don't completly understand how this works)
#Composable
private fun CustomStaggeredVerticalGrid(
// on below line we are specifying
// parameters as modifier, num of columns
modifier: Modifier = Modifier,
numColumns: Int = 2,
content: #Composable () -> Unit
) {
// inside this grid we are creating
// a layout on below line.
Layout(
// on below line we are specifying
// content for our layout.
content = content,
// on below line we are adding modifier.
modifier = modifier
) { measurable, constraints ->
// on below line we are creating a variable for our column width.
val columnWidth = (constraints.maxWidth / numColumns)
// on the below line we are creating and initializing our items
constraint widget.
val itemConstraints = constraints.copy(maxWidth = columnWidth)
// on below line we are creating and initializing our column height
val columnHeights = IntArray(numColumns) { 0 }
// on below line we are creating and initializing placebles
val placeables = measurable.map { measurable ->
// inside placeble we are creating
// variables as column and placebles.
val column = testColumn(columnHeights)
val placeable = measurable.measure(itemConstraints)
// on below line we are increasing our column height/
columnHeights[column] += placeable.height
placeable
}
// on below line we are creating a variable for
// our height and specifying height for it.
val height =
columnHeights.maxOrNull()?.coerceIn(constraints.minHeight,
constraints.maxHeight)
?: constraints.minHeight
// on below line we are specifying height and width for our layout.
layout(
width = constraints.maxWidth,
height = height
) {
// on below line we are creating a variable for column y pointer.
val columnYPointers = IntArray(numColumns) { 0 }
// on below line we are setting x and y for each placeable item
placeables.forEach { placeable ->
// on below line we are calling test
// column method to get our column index
val column = testColumn(columnYPointers)
placeable.place(
x = columnWidth * column,
y = columnYPointers[column]
)
// on below line we are setting
// column y pointer and incrementing it.
columnYPointers[column] += placeable.height
}
}
}
}
Calling above code as below
Column(
// for this column we are adding a
// modifier to it to fill max size.
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.then(layoutModifier)
) {
// on below line we are creating a column
// for each item of our staggered grid.
CustomStaggeredVerticalGrid(
// on below line we are specifying
// number of columns for our grid view.
numColumns = numColumns,
) {
// inside staggered grid view we are
// adding images for each item of grid.
itemList.forEachIndexed { index, singleItem ->
// on below line inside our grid
// item we are adding card.
SomesingleItemCompose(singleItem , singleItemModifier ,index) // this one single grid item Ui as per requirement
}
}
}
As I said above , I was testing paginated data loading in compose staggered layout. It working. Trick was to use and tweak a little 'advance-pagiantion-start' codelab code (manual pagination data handling) and move it to compose. (there is still no way to use pagination library yet)
Solution : https://github.com/rishikumr/stackoverflow_code_sharing/tree/main/staggered-layout-compose-with_manual_pagination
Working video : https://drive.google.com/file/d/1IsKy0wzbyqI3dme3x7rzrZ6uHZZE9jrL/view?usp=sharing
How does it work :
Make a network request , fed it to UI custom staggered layout ( How to achieve a staggered grid layout using Jetpack compose? )
Listen to manual scroll and make a network request (for next page) when it scrolled to the end.
(https://github.com/rishikumr/stackoverflow_code_sharing/blob/main/staggered-layout-compose-with_manual_pagination/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt)
val itemList = remember { mutableStateListOf<Repo>() }
var lastListSize by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
viewModel.fetchContent()
.collect { result ->
when (result) {
is RepoSearchResult.Success -> {
Log.d("GithubRepository", "result.data ${result.data.size}")
itemList.clear()
itemList.addAll(result.data)
}
is RepoSearchResult.Error -> {
Toast.makeText(
this#SearchRepositoriesActivity,
"\uD83D\uDE28 Wooops $result.message}",
Toast.LENGTH_LONG
).show()
}
}
}
}
val scrollState = rememberScrollState()
val endReached = remember {
derivedStateOf {
(scrollState.value == scrollState.maxValue) && (lastListSize != itemList.size) && (scrollState.isScrollInProgress)
}
}
Column(Modifier.verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier = Modifier.size(100.dp)) {
Text("Other Top composable")
}
StaggeredVerticalScreen(
itemList = itemList,
numColumns = 2,
layoutModifier = Modifier.padding(
start = 12.dp,
bottom = 12.dp
),
singleItemModifier = Modifier.padding(
end = 12.dp,
top = 12.dp
)
) { singleGridItem, singleItemModifier, index ->
SingleArticleItem(singleGridItem , index)
}
if (endReached.value) {
lastListSize = itemList.size
Log.d("SearchRepositoriesActivity", "End of scroll lazyItems.itemCount=${itemList.size}")
viewModel.accept(UiAction.FetchMore)
}
}
you have to maintain the current page number and few other things. (https://github.com/rishikumr/stackoverflow_code_sharing/blob/main/staggered-layout-compose-with_manual_pagination/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt)
A. Go through the code lab advance-pagination-start-code and you will understand how it all works. (I removed the part of code for on text change api calls because I didn't needed them)
B. this currently inMemory caching only, I am working on Room database storing. i believe this should not be difficult.
If you have to have to use paging library then we need to embed view inside our compose
OPTION 2: Another option is to inflate view-staggered layout xml in parent compose using 'androidx.compose.ui:ui-viewbinding' library. I also tried with this and this works fantasticaly. Beaware all thigs need to be wrt view, adapter and all
setContent {
// get the view model
val viewModel = ViewModelProvider(
this, Injection.provideViewModelFactory(
context = this,
owner = this
)
)[SearchRepositoriesViewModel::class.java]
AndroidViewBinding(ActivitySearchRepositoriesBinding::inflate) {
val repoAdapter = ReposAdapter()
val header = ReposLoadStateAdapter { repoAdapter.retry() }
list.adapter = repoAdapter.withLoadStateHeaderAndFooter(
header = header,
footer = ReposLoadStateAdapter { repoAdapter.retry() }
)
val staggeredGridLayoutManager =
StaggeredGridLayoutManager(2, LinearLayoutManager.VERTICAL)
this.list.apply {
layoutManager = staggeredGridLayoutManager
setHasFixedSize(true)
adapter = repoAdapter
}
lifecycleScope.launch {
viewModel.pagingDataFlow.collectLatest { movies ->
repoAdapter.submitData(movies)
}
}
retryButton.setOnClickListener { repoAdapter.retry() }
lifecycleScope.launch {
viewModel.pagingDataFlow.collectLatest(repoAdapter::submitData)
}
lifecycleScope.launch {
repoAdapter.loadStateFlow.collect { loadState ->
// Show a retry header if there was an error refreshing, and items were previously
// cached OR default to the default prepend state
header.loadState = loadState.mediator
?.refresh
?.takeIf { it is LoadState.Error && repoAdapter.itemCount > 0 }
?: loadState.prepend
val isListEmpty =
loadState.refresh is LoadState.NotLoading && repoAdapter.itemCount == 0
// show empty list
emptyList.isVisible = isListEmpty
// Only show the list if refresh succeeds, either from the the local db or the remote.
list.isVisible =
loadState.source.refresh is LoadState.NotLoading || loadState.mediator?.refresh is LoadState.NotLoading
// Show loading spinner during initial load or refresh.
progressBar.isVisible = loadState.mediator?.refresh is LoadState.Loading
// Show the retry state if initial load or refresh fails.
retryButton.isVisible =
loadState.mediator?.refresh is LoadState.Error && repoAdapter.itemCount == 0
// Toast on any error, regardless of whether it came from RemoteMediator or PagingSource
val errorState = loadState.source.append as? LoadState.Error
?: loadState.source.prepend as? LoadState.Error
?: loadState.append as? LoadState.Error
?: loadState.prepend as? LoadState.Error
errorState?.let {
Toast.makeText(
this#SearchRepositoriesActivity,
"\uD83D\uDE28 Wooops ${it.error}",
Toast.LENGTH_LONG
).show()
}
}
}
}
}
I have both the sample (option 1 and option 2) working and tested. There is no flickering no abrupt behaviour so far. Its working good, with both options. let me know if there any thing I could help. (Also this is my first answer yeyy...!!)

How to remember that the scrollable content has been reached?

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
}

Is there any way to make two horizontal pager of accompanist library to work synchronously?

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:

Execute LaunchedEffect only when an item is visible in LazyColumn

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
}

Bad behavior from Jetpack compose HorizontalPager

I have a problem with the HorizontalPager of the new framework Jetpack Compose. When I associate the HorizontalPager with a TabRow, everything works fine until I reach the last element of my TabRow (Title3) (by clicking the tab row or swipping till the last page). When I am on the last page, if I swip for 1 nanometer to the left the pager switch from last page (Title3) to the previous one (Title2). It is not like other switch between pages, this one is instantaneous when others are continue. I hope I am enough clear to be understandable. Here is the code I use:
val tabTitles = listOf("Title1", "Title2", "Title3")
val pagerState = rememberPagerState(pageCount = tabTitles.size)
val coroutineScope = rememberCoroutineScope()
Column {
TabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(Modifier.pagerTabIndicatorOffset(pagerState, tabPositions))
}) {
tabTitles.forEachIndexed { index, title ->
Tab(
text = { Text(title) },
selected = pagerState.currentPage == index,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } }
)
}
}
HorizontalPager(state = pagerState) { page ->
LazyVerticalGrid(
cells = GridCells.Fixed(2),
contentPadding = PaddingValues(4.dp)
) {
when (page) {
0 -> // Create a list of card for each item
1 -> // Create a list of card for each item
2 -> // Create a list of card for each item
}
}
}
}
I hope someone can help me even if this is a new framework :/
I am open to other solution to implement this kind of pager without the HorizontalPager if the problem come from it and not from my code.
Thanks in advance.
Edit: It was a bug from google accompanist library on version 0.9.0, I solved my problem using the newest version (0.10.0).

Categories

Resources