Migrating to the Compose PullToRefreshBox

Pull to Refresh is a common pattern we find across mobile apps – it allows our users to refresh the content of screen in a single swipe. Jetpack Compose has provided support for this for some time, through the pullRefresh modifier. In your code, this would look something like the following:

val pullRefreshState = rememberPullRefreshState(isRefreshing), { (handle refresh) })

Box(
    modifier.pullRefresh(pullRefreshState)
) {

    PullRefreshIndicator(
        modifier = Modifier.align(Alignment.TopCenter),
        refreshing = isRefreshing,
        state = pullRefreshState
    )
}

While this was great to initially support pull to refresh in Compose UI, it provided several points of friction:

  • Low discoverability – it is not immediately obvious that a pullRefresh modifier would be available to use, adding an extra step in for developers to search for its availability
  • Boilerplate code – alongside adding a Box container with corresponding modifier, we have to add the use of the PullRefreshIndicator composable as a child of the Box. This results in developers having to add several pieces of code for something that was previously supported out of the box in the Android View System
  • Difficult to customise – while the modifier provides us with access to this functionality, there is friction when it comes to customising the indicator
  • Reduced readability – having to add a parent Box to any layout is fine, but it isn’t immediately clear why this has been added until you see the pullRefresh modifier

We can see here that there are several points here which demonstrate how the API design for the original pull to refresh support was not as simple as it could have been. However, I am still grateful that I was able to add support for this to my apps – which is an OK trade off for me!

Fast forward to now and we have access to a new experimental composable, the PullToRefreshBox. It’s important to note that this in an experimental state, but you’ll be able to access it in the latest version of the compose material 3 package. When using the PullToRefreshBox, you’ll want to do something like the following:

PullToRefreshBox(
    modifier = modifier,
    isRefreshing = isRefreshing,
    onRefresh = onRefresh
) {

}

From this example, we can already see how some of the previous friction points are already cleared up:

  • Improved discoverability – when looking for functionality in compose, I will initially attempt to type my desired functionality to see if there is a composable that exists (this is what I actually remember doing for pull-to-refresh originally!). With this new composable, developers will be able to discover support for this functionality when composing UI
  • Improved readability – when scanning through code, this composable will make it much clearer what the purpose is of the composable, without having to rely on reading modifiers
  • Less boilerplate code – with this composable, for out-of-the-box support, we no longer need to declare the indicator to be used or provide a reference to any pull refresh state. This results in much less code being added to our codebase

As we can see from the example, we still need to provide a reference to an isRefreshingState that is used to depict if the refreshing indicator should be shown, along with an onRefresh callback for when the refresh operation is triggered. But with the above points, I am in full support of this composable and will be using this to simplify the pull-to-refresh implementations that I have in place (once this hits stable!).

One of the other friction points we touched on above was existing modifier being difficult to customise – how does this new composable make this easier? The PullToRefreshBox composable support an indicator argument that allows us to provide an indicator for use as the refreshing indicator. When using this, we’ll need to provide the PullToRefreshState to the indicator, so we’ll need to declare this using the rememberPullToRefreshState function. This is so that we can use the same state reference to the parent PullToRefreshBox and the indicator to be used. As per the example above, if you are not customising the indicator, then you do not need to provide this PullToRefreshState.

val state = rememberPullToRefreshState()

PullToRefreshBox(
    modifier = modifier,
    state = state,
    isRefreshing = isRefreshing,
    onRefresh = onRefresh,
    indicator = {
    
    }
) {

}

When it comes to customising this indicator, we have a couple of options – which one you use will depend on how much customisation you are looking for! If we simply want to customise the colors used for the indicator, you can use the provided Indicator composable, providing values for both the containerColor and color arguments. We will also need to provide the value for the isRefreshing argument which we passed to the parent PullToRefreshComposable, along with the shared PullToRefreshState for the state. Providing this composable for the indicator argument will override the default indicator being used when the user performs the gesture.

Indicator(
    modifier = Modifier.align(Alignment.TopCenter),
    isRefreshing = isRefreshing,
    containerColor = MaterialTheme.colorScheme.primaryContainer,
    color = MaterialTheme.colorScheme.onPrimaryContainer,
    state = state
)

If you want to provide a completely custom indicator composable, then you can do so using the pullToRefreshIndicator modifier. This modifier can be applied to the composable that defines your indicator, for which you will need to provide the same state arguments as above to reflect the state of the indicator.

pullToRefreshIndicator(
    state = state,
    isRefreshing = isRefreshing,
    containerColor = PullToRefreshDefaults.containerColor,
    threshold = PositionalThreshold
)

From this post we can see how much the PullToRefreshBox simplifies pull-to-refresh within our Jetpack Compose UI. I recommend you checking this out and preparing to migrate when it feels in a stable enough state for your own projects!