Pull-to-Refresh featured image

How to Add Pull-to-Refresh to Your Android App with Jetpack Compose

In this post, I will show you how to use the PullRefreshIndicator composable to add Pull to refresh behavior to your list.

Here is a GIF of what we are going to create:

Prequesties

In your app-level build.gradle add the following to your dependencies {...}:

dependencies {
    // that is the library that contains the PullRefreshIndicator composable
    implementation("androidx.compose.material:material:1.5.3") // or later version
    // These are used to use viewmodel and flows with Compose
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
}

You only need implementation("androidx.compose.material:material:1.5.3"), but in my code, I will use ViewModel and will collect a Flow as a state so the other libraries added for this purpose


Implementation

Create PullRefreshState state

First, you will need to create a remembered PullRefreshState object:

val pullRefreshState = rememberPullRefreshState(
    refreshing = mainViewModel.isRefreshing,
    onRefresh = {
        mainViewModel.getItems()
    })
MainActivity.kt

Let’s break it down:
  • The rememberPullRefreshState is a function from the compose material library that helps you create and remember the object.
  • refreshing: A boolean representing whether a refresh is currently occurring. When it is true, the indicator will be visible and animating. In our case, we are reading a mutable state in the mainViewModel as you will see later
  • onRefresh: a callback that will fire when a user pulls down and triggers a refresh, in this case, we are calling a function on mainViewModel to get the items

Adding the PullRefreshIndicator Composable

Next, you need to put your list along with the List inside a Box composable as follows:

// Box is used to place the pull to refresh indicator on top of the content
Box(Modifier.pullRefresh(pullRefreshState)) {
        // Your list or content
        ItemsList(data = items, modifier = Modifier.fillMaxSize())
        // the pull to refresh indicator
        PullRefreshIndicator(
            refreshing = mainViewModel.isRefreshing,
            state = pullRefreshState,
            modifier = Modifier.align(Alignment.TopCenter),
            contentColor = MaterialTheme.colorScheme.primary,
        )
    }
MainActivity.kt

Let’s break it down:
  • Modifier.pullRefresh(pullRefreshState) is a nested scroll modifier that provides scroll events to the state.
  • ItemsList is a composable that contains LazyColumn which shows our items.
  • PullRefreshIndicator is the composable of the pull-to-refresh component.
  • refreshing A Boolean represents whether a refresh is occurring.
  • modifier = Modifier.align(Alignment.TopCenter) this will make the indicator show on the top center, you can change it as you like.

Here is the Full code:


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.codinginsideout.pulltorefresh.ui.theme.PullToRefreshTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PullToRefreshTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    HomeScreen(modifier = Modifier.fillMaxSize())
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun HomeScreen(
    modifier: Modifier = Modifier,
    mainViewModel: MainViewModel = viewModel()
) {
    val pullRefreshState = rememberPullRefreshState(
        refreshing = mainViewModel.isRefreshing,
        onRefresh = {
            mainViewModel.getItems()
        })
    val items by mainViewModel.items.collectAsStateWithLifecycle(initialValue = emptyList())

    // Box is used to place the pull to refresh indicator on top of the content
    Box(modifier.pullRefresh(pullRefreshState)) {
        // Your list or content
        ItemsList(data = items, modifier = Modifier.fillMaxSize())
        // the pull to refresh indicator
        PullRefreshIndicator(
            refreshing = mainViewModel.isRefreshing,
            state = pullRefreshState,
            Modifier.align(Alignment.TopCenter),
            contentColor = MaterialTheme.colorScheme.primary,
        )
    }
}

@Composable
fun ItemsList(data: List<String>, modifier: Modifier = Modifier) {
    LazyColumn(
        modifier = modifier,
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        items(data) {
            Card(modifier = Modifier.fillMaxWidth()) {
                Text(text = it, modifier = Modifier.padding(16.dp))
            }
        }
    }
}
MainActivity.kt

The Data Layer

That is just our ViewModel. In a real app, you would have a repository, models, and UiState object.


import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {
    var isRefreshing: Boolean by mutableStateOf(false)
        private set

    private val _items: MutableStateFlow<List<String>> = MutableStateFlow(emptyList())
    val items: Flow<List<String>> = _items.asStateFlow()

    init {
        getItems()
    }

    fun getItems() {
        viewModelScope.launch {
            isRefreshing = true
            /*
             Here you would get data from an api or database
             but for this example we will just generate some random data
             */
            val items = (0..100).shuffled().map { "Item $it" }
            delay(1000) // simulate network
            _items.value = items
            isRefreshing = false
        }
    }
}
MainViewModel.kt

As you can see the isRefreshing is a mutable state which is very important so changing it triggers the PullRefreshIndicator to recompose and show on the screen. The getItems is nothing special. It just simulates fetching data.

Conclusion

In this post, we have learned how to use the PullRefreshIndicator composable to add a pull-to-refresh feature to our list in a Jetpack Compose app. We have seen the necessary steps and code snippets to implement this behavior.

First, we added the required dependencies to our build.gradle file. Then, we created a PullRefreshState object using the rememberPullRefreshState function, which keeps track of the refresh state and provides a callback for when a refresh is triggered.

Next, we wrapped our list and the PullRefreshIndicator in a Box composable, using the Modifier.pullRefresh function to handle scroll events and pass them to the state. The PullRefreshIndicator component takes the refreshing state, the pull refresh state object, and other modifiers to customize its appearance.

We also provided the full code for the example, including the MainActivity and the HomeScreen composable, which uses the ItemsList component to display the list of items. Additionally, we included the MainViewModel that handles the refresh logic and provides the data to the UI.

Remember, these code snippets are just a starting point, and in real-world scenarios, you would have more complex data layers with repositories and models.

By following the steps outlined in this post, you can easily add the pull-to-refresh functionality to your Jetpack Compose app and provide a better user experience. So go ahead and give it a try in your own projects!

You can find the full code in this GitHub repository.

1 thought on “How to Add Pull-to-Refresh to Your Android App with Jetpack Compose”

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top