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).
Related
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...!!)
I implemented a simple HorizontalPager which works mostly as expected. The app I develop is for one specific device, a 8" Tablet in landscape mode. At the moment it is required to swipe more than 50% of the screen-width to get to the next/prev page. This is a very long swipe and I would like to reduce that to make changing pages easier...
I played around with fling behavior and tried to manually change pages when the offset changed or to intercept the touchevents...Nothing really lead to the desired behavior.
Since the "problem" seems so simple, I really hope that I have just overseen something. Do you have an idea what I could try?
This solved my problem, you can edit minFlingDistanceDp to change the sensitivity:
HorizontalPager(
modifier = Modifier.fillMaxSize(),
state = pagerState,
count = noOfPages,
flingBehavior = flingBehavior(pagerState = pagerState, noOfPages = noOfPages)
) { page ->
//Content
}
val minFlingDistanceDp = 150.dp
#OptIn(ExperimentalPagerApi::class, dev.chrisbanes.snapper.ExperimentalSnapperApi::class)
#Composable
fun flingBehavior(pagerState: PagerState, noOfPages: Int): FlingBehavior {
var currentPageIndex = remember { pagerState.currentPage }
return PagerDefaults.flingBehavior(
state = pagerState,
snapIndex = { layoutInfo, _, _ ->
val distanceToStartSnap = layoutInfo.distanceToIndexSnap(currentPageIndex)
currentPageIndex = when {
distanceToStartSnap < -(minFlingDistanceDp.value) -> {
(currentPageIndex + 1).coerceAtMost(noOfPages - 1)
}
distanceToStartSnap > minFlingDistanceDp.value -> {
(currentPageIndex - 1).coerceAtLeast(0)
}
else -> {
currentPageIndex
}
}
currentPageIndex
}
)
}
I am using Jetpack Compose, along with Paging 3 library & Jetpack Navigation. The issue I am facing is I have a LazyList which is fetching data from remote source using paging library.
ViewModel
fun getImages(): Flow<PagingData<ObjectImage>> = Pager(
PagingConfig(PAGE_SIZE, enablePlaceholders = false)
) { DataHome(RANDOM) }.flow.cachedIn(viewModelScope)
HomeView
val images = viewModelHome.getImages().collectAsLazyPagingItems()
LazyColumn {
...
}
Now whats happening is when I navigate to another View using navHostController.navigate() and then press back to get to HomeView... the LazyColumn resets itself & start loading items again from network.
So I am stuck with this issue. I tried manually caching in viewModel variable... though it works but it screws up SwipeRefresh (which stops showing refresh state)
data.apply {
when {
// refresh
loadState.refresh is LoadState.Loading -> {
ItemLoading()
}
// reload
loadState.append is LoadState.Loading -> {...}
// refresh error
loadState.refresh is LoadState.Error -> {...}
// reload error
loadState.append is LoadState.Error -> {...}
}
}
implementation("androidx.paging:paging-runtime-ktx:3.1.0")
implementation("androidx.paging:paging-compose:1.0.0-alpha14")
Is this an issue with PagingLibrary which is still in alpha??
Update 1 (I am not sure if this is a good solution, but I am solving
the swipe refresh issue as follows)
// get images
var images: Flow<PagingData<ObjectImage>> = Pager(PagingConfig(PAGE_SIZE)) {
DataHome(RANDOM)
}.flow.cachedIn(viewModelScope)
// reload items
fun reload(){
images = Pager(PagingConfig(PAGE_SIZE)) {
DataHome(RANDOM)
}.flow.cachedIn(viewModelScope)
}
// and rather than calling .refresh() method on lazy items... I am calling viewModel.reload()
The problem is that you are creating new Pager every time you call getImages(), which is every time your composable recomposes, that's not how it's supposed to be done.
You should make it a val items = Pager(... for the caching to work.
For the screwed up SwipeRefresh, how do you implement it? There is a refresh() method on LazyPagingItems, you should use that.
EDIT: Ok, so based on the coments and edits to your question:
In your viewmodel, do as I suggested before:
val items = Pager( // define your pager here
Your composable can then look like this:
#Composable
fun Screen() {
val items = viewModel.items.collectAsLazyPagingItems()
val state = rememberSwipeRefreshState(
isRefreshing = items.loadState.refresh is LoadState.Loading,
)
SwipeRefresh(
modifier = Modifier.fillMaxSize(),
state = state,
// use the provided LazyPagingItems.refresh() method,
// no need for custom solutions
onRefresh = { items.refresh() }
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
// display the items only when loadState.refresh is not loading,
// as you wish
if (items.loadState.refresh is LoadState.NotLoading) {
items(items) {
if (it != null) {
Text(
modifier = Modifier.padding(16.dp),
text = it,
)
}
}
// you can also add item for LoadState.Error, anything you want
if (items.loadState.append is LoadState.Loading) {
item {
Box(modifier = Modifier.fillMaxWidth()) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp)
)
}
}
}
}
// if the loadState.refresh is Loading,
// display just single loading item,
// or nothing at all (SwipeRefresh already indicates
// refresh is in progress)
else if (items.loadState.refresh is LoadState.Loading) {
item {
Box(modifier = Modifier.fillParentMaxSize()) {
Text(
text = "Refreshing",
modifier = Modifier.align(Alignment.Center))
}
}
}
}
}
}
I'm trying to implement Grid Layout with 2 columns using Compose, but LazyVertical Grid does not work for me. I searched for some workarounds to fulfill the task, but nothing was rendered on a screen. Any ideas?
val state = rememberLazyListState()
LazyVerticalGrid(
cells = GridCells.Fixed(2),
state = state,
content = {
items(bookList.books){
bookList.books.map {
BookUI(book = it, onClick = {})
}
}
}
)
I tried using LazyVerticalGrid this way, but it does not render a list, while LazyColumn renders it
You don't need a map when using items.
Change
items(bookList.books){
bookList.books.map {
BookUI(book = it, onClick = {})
}
}
to
items(bookList.books){ book ->
BookUI(book = it, onClick = {})
}
Don't forget to import,
import androidx.compose.foundation.lazy.items
try to use the following code:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun MyGrid(items: List<String>) {
LazyVerticalGrid(
cells = GridCells.Fixed(count = 2)
) {
items(items) { text ->
Text(text = text)
}
}
}
Few things you should pay attention to:
the items(*) {} function need to be imported from androidx.compose.foundation.lazy.items
You added #OptIn(ExperimentalFoundationApi::class)
rememberLazyListState() is actually a default param so no need to add it.
For the above example you can use something like this:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun Content() {
MyGrid(
items = listOf(
"Item A",
"Item B",
"Item C",
"Item D",
"Item E",
"Item F"
)
)
}
And you will get this:
Implementation has changed since the time your question was created, so the following code snipped should work in your case:
val state = rememberLazyGridState()
LazyVerticalGrid(
state = state,
columns = GridCells.Fixed(2)
) {
items(bookList.books) { books ->
BookUI(book = it, onClick = {})
}
}
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: