Creating a ViewPager in Jetpack Compose


In this article we will find out how to write a ViewPager composable in Jetpack Compose. Here’s what we want our composable to offer:

  • The selected item will be centered on the container.

From our set of requirements above it is clear we will need to use the Layout composable to create our Pager, this is akin to creating a custom ViewGroup in the view system — this will give us the flexibility to control the measuring and positioning of the children in the Pager.

The Pager signature

Now that we have a clear set of requirements, let’s define the signature of our Pager composable function:

Let’s have a look at this in detail:

  1. Our composable Pager takes first a list of items, of type T. Our pager will accept items of the same type; we will see later how we can modify this to accept heterogeneous types.

Setting up some helper methods

As we have to handle both horizontal and vertical scroll, we will need to check the orientation parameter during the composition process, and based on its value either get a width or a height. To make our code more readable we will extract this into extensions functions, so let’s define those now so that we can reference them later. These are the functions that we will use throughout the Pager composition:

This function returns the maximum dimension on our Constraints based on our scroll orientation.

This function makes a copy of our Constraints where the minimum dimension not in the scroll direction is set to 0, while on the scroll direction we set the minimum dimension to be equal to the maximum dimension. Note also that we are making use of the itemFraction to constrain the Pager items to a fraction of the container dimension in the scroll direction.

Our next helper function gets the size of our Parcelables based on the scroll direction. If we are scrolling horizontally, then we want to know the maximum height of the Pageritems; otherwise, if we are scrolling vertically, we want to calculate the maximum width.

Next we have an extension function on VelocityTracker that will provide the calculated velocity based on our scroll axis.

Our final helper method, similar to the previous ones, calculates the pointer input change based on our scroll direction.

Creating the Pager state

Now that we have our Pager signature defined and our helper functions it’s time to create our Pager state. For simple composables we can keep state within the composable function itself, but when composables are a bit more complex, like this one, it is preferable to create a class to host the state and reference that class from within the composable. This state is remembered in the composable, leveraging the remember functions in Jetpack Compose.

Let’s see our state class:

Our state closely maps the arguments we pass to our Pager composable. We will reference these elements during composition and animation, and as our Pager composable needs to react to changes to them, we wrap them in mutableStateOf. Worth of note is the dragOffset that we will use to allow the user to drag the Pager items and fling them; the dragOffset will indicate, in pixels, how much the user has dragged the Pager content.

When we create a state class it is also customary to create a remember function that wraps our state into a remember lambda, and call that from the composable. Let’s add this function and see how we are calling it from our composable:

This is fairly straightforward, I’ll just point out that, by convention, Composable functions that return a value, like in this case, should not use Pascal case.

And finally let’s see how we call this from our composable:

So after the sanity checks we get an instance of our state and we initialize it with the arguments we receive. We are converting the item spacing from DPs to pixels as all measurements take place in pixels; converting it here means we don’t have to do it multiple times later.

We also get our coroutine scope as we will need it for the dragging and flinging operations.

A small thing worth of note is that our state listener takes an index representing the position of the selected item, but the composable caller instead expects the item, so we map the index to an item when calling the listener.

Measuring and laying the content

Next we will see how we measure the items to display on the Pager and how to position them, based on the parameters we receive and the current drag offset. For this we will use the Layout composable; this composable takes 3 arguments — the content to display, a Modifier and a lambda that does the actual measuring and laying of the content. Let’s look at our code and then we’ll break it down into these sections:

Let’s have a look at each block now, let’s start with the content:

Here we are using the content factory to generate the composables for each of the pages in our Pager. We wrap each composable in a Box so that we can ensure that the pager item will fill the parent, either horizontally (for horizontal scroll), or vertically (for vertical scroll). In case the content is not set to fill the parent, we use the Alignment.Center to ensure the actual content is centered in the Box.

Next the modifier:

There are 2 attributes here, first we clip the content to the bounds (so that if the user is dragging, the content does not draw outside the container box), and we apply an inputModifier from our state — we have not seen that yet, but this is what will handle dragging and calculate the drag offset to display our Pager items. For now, let’s just take it at face value that this will update the drag offset, we will see in a bit how this works.

Finally, let’s look at the measure block, this is where we measure and place the children in our Pager composable:

This section is a bit more complex, so let’s walk it in detail:

  1. We first get the size of our composable, using our helper method. Here we check the orientation and we get the max width or height from the Constraints.

With this our Pager is ready to display our content. If we were to run this (removing the state’s inputModifier as it’s not yet defined) we would see the items displayed with the selected item centered and, if the item fraction is less than 1, the neighbouring items peeking out on the sides.

Our next steps is to add the logic to drag and fling the content. Let’s check that out.

Input handling

Now that we have the measuring and laying out logic in place we need to add the necessary logic to handle dragging and flinging. We will do this in our state class:

Let’s have a detailed look at how we handle input events on the Pager:

  1. We define an animation spec for the fling behaviour, we use a Spring animation that will slow down and come to a rest. We specify LowBouncy so that there is a bit of wobble when coming to a rest, but not much, and a StiffnessLow so that the Pager does not come to rest too soon.

With this we are almost done, there is just one last item to handle.

Selecting an initial item

One of our requirements was to be able to select which item would be initially selected in the Pager. For that, we will add a helper method in our state to snap to that position:

In this method we calculate the drag offset for the item at the requested position, then set our drag offset to that value immediately, without animation, using the snapTo method.

The last step is to call this from our Pager composable. Because this is something we only want to do once, on first composition, we will use an Effect, specifically a LaunchedEffect — these trigger upon initial composition, and whenever the keys change:

Because our initially selected index depends on the items and the index itself, we use those as keys for the effect.

Completed Pager

We have now our Pager fulfilling all the requirements we described a while back. The overall implementation is shown below:

This is how we can use it:

and this is the final result:

Using heterogeneous items

This implementation of the Pager takes a factory that builds the composables that we will display in the pager. Let’s update our Pager to instead accept a set of Composables. The changes are fairly straightforward, only the Pager itself needs to change, the state remains the same. Let’s see how we can update our Pager to accommodate this scenario:

There are only some small changes here:

  • Our Pager is no longer typed.

With this update, we can call our pager like so:

where we alternate between a Box composable and an Image composable in our Pager.

There is however a lot of common code between the 2 Pager variants, so we can consolidate much of it by calling one Pager from the other. We will refactor our original Pager to call the new one:

So we have refactored our original Pager by calling the new Pager with the content being a lambda that calls our factory method. We also provide an item selected listener that translates the index into the corresponding item.

With this our work is complete, we have 2 different Pager implementations that satisfy different needs.

The final code with the 2 Pagers is available on this gist.

Final thoughts

This Pager is not a full replacement for the ViewPager in the view system, some of the functionality is missing, for instance item transformations and updates on scroll state (idle, settling, scrolling) to name a couple.

This implementation, while somewhat basic, handles the core pager functionality and offers some customization parameters that will hopefully satisfy a wide range of use cases.

With this article I intended to showcase how to build new components in Jetpack Compose and showcase how to handle animations and user input in those components, I hope you found that useful and I’ll see you on the next article.

Senior Android Developer