Jetpack Compose: HorizontalPager with PagerIndicator & Infinity scroll

Constantinos Levi Ibrushi
XM Global
Published in
4 min readMay 29, 2023

In this article, we will explore the process of creating an infinite HorizontalPager with a PagerIndicator by using Compose.

Android | Kotlin | Jetpack Compose | ViewPager | PagerIndicator

As you may already know, Jetpack Compose does not have a built-in ViewPager component. However, we can incorporate ViewPager functionality into our project by adding the respective accompanist library dependency to our build.gradle file.

implementation "com.google.accompanist:accompanist-pager:0.28.0"

In order to incorporate indicators, we will also utilize the respective library from accompanist.

implementation "com.google.accompanist:accompanist-pager-indicators:0.28.0"

Note: For this project, we are using Compose version 1.3.1 and Kotlin version 1.8.10.

Let’s begin by creating a HorizontalPager.

Thanks to the accompanist library, creating a HorizontalPager is a simple task.

HorizontalPager(
count = pageCount,
state = pagerState,
modifier = modifier
) {
// page content
}
  • Count: The number of pages

We set the count to a significantly large number, such as Int.MAX_VALUE, so we can achieve infinite scrolling behavior.

// Used Int.MAX_VALUE for infinity scroll
val pageCount = Int.MAX_VALUE
  • State: The state object that is used to control or observe the pager’s state.

For state creation, we only need the initialPage value. In order to enable infinite scrolling in both directions, we should start from the middle of the given page count. Therefore, the initialPage can be set as in the following example:

val middlePage = pageCount / 2
val pagerState = rememberPagerState(initialPage = middlePage)

While everything may seem correct at first glance, we will soon encounter an issue concerning the initial displayed page.

The Trick 🪄

In order to achieve infinite behavior for the ViewPager, we implemented a solution by setting the count to a very large number. However, our actual list of items (trophies) is considerably smaller. To ensure that the ViewPager displays the correct page from our real list without creating duplicate pages, we need to handle the page number appropriately.

To address this challenge, we divide the provided page number by the size of the trophies list. The remainder of this division allows us to obtain the correct page index within our real list. By performing this calculation, we ensure that the ViewPager only displays the actual items from the list, preventing any duplications.

By utilizing this approach, we can seamlessly navigate through the trophies list within the ViewPager while maintaining its infinite behavior.

Take a look at the following example:

val realSize = trophies.size

HorizontalPager(
count = pageCount,
state = pagerState,
modifier = modifier
) { page ->
val realPage = page % realSize
// max value is trophies.size
TrophyWidget(realPage, trophy = trophies[realPage])
}

Did you get it? No? 🤔 (Let's do the maths then!)

ViewPager position

Looks like our math is working just fine! 🎉

If you take a closer look, it becomes apparent that the initial page is not the first on the trophies list. And actually, the initial state depended on the trophies list size. To resolve this discrepancy and ensure the correct initial state, it is necessary to calculate and pass a parameter to the ViewPager state.

val realSize = trophies.size

val middlePage = pageCount / 2
// Init the PagerState with a very large number and make it always start from the first item of the real list
val pagerState = rememberPagerState(initialPage = middlePage - (middlePage % realSize))

By decreasing the middlePage with the result of the mod of middlePage and trophies size we are making sure that the ViewPager is going to start from the begging of the trophies list.

Pager Indicator

Adding indicators is also very simple, we just need to add HorizontalPagerIndicatorand pass pagerStateas a parameter.

Android | HorizontalPager | Indicator

⚠️ However, there is an issue here! If you try to use the pagerState without specifying the real size of the list (trophies), then the app will 🧨 . That’s because the default pageCount of HorizontalPagerIndicator is set to the value of PagerState.pageCount, which, in our case, is a very large number.

Fortunately, we can specify the pageCount by adding it as a parameter in HorizontalPagerIndicator.

Take a look at the example:

HorizontalPagerIndicator(
pagerState = pagerState,
pageCount = realSize,
pageIndexMapping = { page -> page % realSize },
activeColor = Color.White,
modifier = modifier
.align(Alignment.BottomCenter)
.padding(bottom = 12.dp)
)

Also we have to describe how to obtain the position of the active indicator by passing the page into pageIndexMapping function. This can be achieved by dividing pagerState.currentPage by the size of the trophies list.

As you can see in the example above, this can be implemented with the following code snippet:

pageIndexMapping = { page -> page % realSize }

To get the position of the active indicator, you can use the pageIndexMapping function and perform the mod using pagerState.currentPage and the size of the trophies list.

Auto-Scroll

If you need also your pager to auto-scroll, you can use the following code snippet:

// Start auto-scroll effect
LaunchedEffect(isDraggedState) {
// convert compose state into flow
snapshotFlow { isDraggedState.value }
.collectLatest { isDragged ->
// if not isDragged start slide animation
if (!isDragged) {
// infinity loop
while (true) {
// duration before each scroll animation
delay(5_000L)
runCatching {
pagerState.animateScrollToPage(pagerState.currentPage.inc() % pagerState.pageCount)
}
}
}
}
}

Let’s put it all together

--

--