DEV Community

Oğuz Şahin
Oğuz Şahin

Posted on

What Is Paging3 (MVVM, Flow, DataBinding, Hilt)?

developer.android.com

Introduction

Undoubtedly, Recyclerview is one of the most used components while developing an android app. We use Recylerview to show lists of data on one or more pages of applications. When the size of these datasets increases, we need to pay attention to the effective use of system resources and the smooth progress of UI performance. In this article, we will learn the effective and correct way to display large datasets in recylerview. The topics we will learn in the content of the article will be as follows:

  • What is pagination and why do we use it?
  • What is Paging3 and what advantages does it offer?
  • Understanding and implementing Paging3

What is pagination and why do we use it?

Lets’s consider apps like Instagram, Twitter or Facebook. On the main stream pages, they show huge, even endless data on the list. Instead of loading this big data in one go,when the user scrolls to see more of the content displayed on the screen; they do it by loading the new data into the list. Here is the answer to what is pagination actually this logic. The process of loading this dataset in chunks, instead of loading the entire dataset at once, when displaying large datasets on a list.

So why do we need such a process or what does this method do for us?

  • Efficient use of your application’s network bandwidth and system resources
  • Get data on the page faster
  • Less memory usage
  • Not consuming resources for useless data

Understanding and implementing Paging3?

Paging3 is a jetpack library that allows us to easily load large datasets from the data source (local, remote, file..etc).It loads data gradually, reducing network and system resources usage. It is written in Kotlin and works in coordination with other Jetpack libraries. It supports Flow, LiveData and RxJava along with Kotlin Coroutine. It also provides support for many functions that you need to implement manually when you need to load data:

  • Keeps track of the keys to be used for retrieving the next and previous page.
  • Automatically requests the correct page when the user has scrolled to the end of the list.
  • Ensures that multiple requests aren’t triggered at the same time.
  • Allows you to cache data: if you’re using Kotlin, this is done in a CoroutineScope; if you're using Java, this can be done with LiveData.
  • Tracks loading state and allows you to display it in a RecyclerView list item or elsewhere in your UI, and easily retry failed loads.
  • Allows you to execute common operations like map or filter on the list that will be displayed, independently of whether you're using Flow, LiveData, or RxJava Flowable or Observable.
  • Provides an easy way of implementing list separators.

Understanding and implementing Paging3

I created a sample repo for this episode. This repo gets user information from a free API that creates random users. Through this repo, we will both get to know the components of Paging3 and learn how to use it.


1. Adding Dependencies

Let’s start by implementing our necessary dependencies first. Paging 3 also has support for rxjava, guava and jetpack compose.
If you wish, you can add your dependencies from the link according to your needs.

dependencies {

    .....

    // OkHttp interceptor
    implementation "com.squareup.okhttp3:logging-interceptor:$okHttp_interceptor_version"

    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

    //Paging
    implementation "androidx.paging:paging-runtime-ktx:$paging_version"

    //Hilt
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"

    //Glide
    implementation "com.github.bumptech.glide:glide:$glide_version"
    annotationProcessor "com.github.bumptech.glide:compiler:$glide_version"

    //Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_core_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_android_version"

    //ktx
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_ktx_version"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_ktx_version"
    implementation "androidx.activity:activity-ktx:$activity_ktx_version"

    }

Enter fullscreen mode Exit fullscreen mode

2. Network Operations

Let’s start by retrofit process to get user information from the remote server.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    private const val CONNECT_TIMEOUT = 20L
    private const val READ_TIMEOUT = 60L
    private const val WRITE_TIMEOUT = 120L


    @Provides
    @Singleton
    fun provideOkHttpClient(@ApplicationContext context: Context): OkHttpClient {
        val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
            level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
            else HttpLoggingInterceptor.Level.NONE
        }
        return OkHttpClient.Builder()
            .addInterceptor(httpLoggingInterceptor)
            .addInterceptor(NetworkConnectionInterceptor(context))
            .connectTimeout(CONNECT_TIMEOUT, SECONDS)
            .readTimeout(READ_TIMEOUT, SECONDS)
            .writeTimeout(WRITE_TIMEOUT, SECONDS)
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BuildConfig.API_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .build()
    }


    @Provides
    @Singleton
    fun provideUserService(retrofit: Retrofit): UserService {
        return retrofit.create(UserService::class.java)
    }

}

Enter fullscreen mode Exit fullscreen mode

Let’s create the Retrofit and Okhttp classes as a singleton.
By using the dependency injection method with Hilt, we will provide the required class dependencies in a SOLID way. Next, let’s define our service class and model classes.

interface UserService {

    @GET(".")
    suspend fun getUsers(
        @Query("page") page: Int,
        @Query("results") results: Int,
    ): UserResponse

}

Enter fullscreen mode Exit fullscreen mode

Here it is useful to understand the two parameters required by the api.
It specifies the page number requested in each request and the number of items to be loaded at once. Logically, we will load the new page when the user reaches the end of each list, so here we will pull the new data according to the page number.

data class UserResponse(
    val results: ArrayList<UserModel>
)

data class UserModel(
    val name: NameModel,
    val email: String,
    val phone: String,
    val picture: PictureModel
)

data class NameModel(
    val title: String,
    val first: String,
    val last: String
)

data class PictureModel(
    val thumbnail: String
)

Enter fullscreen mode Exit fullscreen mode

3.PagingSource

Let’s start by preparing our PagingSource class for the process of pulling the data after we deal with the network side where we will get the user information.

class UserPagingDataSource(private val userService: UserService) :
    PagingSource<Int, UserModel>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UserModel> {
        val page = params.key ?: STARTING_PAGE_INDEX
        return try {
            val response = userService.getUsers(page, params.loadSize)
            LoadResult.Page(
                data = response.results,
                prevKey = if (page == STARTING_PAGE_INDEX) null else page.minus(1),
                nextKey = if (response.results.isEmpty()) null else page.plus(1)
            )
        } catch (exception: Exception) {
            return LoadResult.Error(exception)
        }
    }


    override fun getRefreshKey(state: PagingState<Int, UserModel>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }

    companion object {
        private const val STARTING_PAGE_INDEX = 1
    }

}

Enter fullscreen mode Exit fullscreen mode

The PagingSource class is a component that comes with Paging3. This class is an abstract generic class that is responsible for the source of the paginated data and how to retrieve data from that source. In this article, we use the PagingSource class because we only provide data from a single source (remote , local, file ..etc). If we were to work in a structure that is both remote and local based (where we mean the process of writing the data in the remote to locale and going through a single source), we would have to use RemoteMediator.
Since we will not mention the concept of RemoteMediator in this article, you can access the necessary information from the link if you wish.

Note: You can use RxPagingSource with rxJava or ListenableFuturePagingSource for guava.

The PaginSource class has two parameter types as generic.

key : **As a key, our API service is index-based, so we will specify it as Int.
**value:
You need to give it as the type of data you will upload. For our example, we can specify it as UserModel.

The Load function is the main function responsible for loading the data to us. This function, as soon as the user reaches the end of the list, takes the next key and sends the request for the new list asynchronously, and this is done automatically by the Paging library. In addition, it is a suspend function and provides a nice structure for us to make network requests in the background.

It takes the LoadParams class named params as a parameter, which holds the page number to be loaded and the number of items to be loaded. From here, we will be able to easily submit our request with the correct page and the number of items to be loaded.During the first load, the key will be null(If you do not specify an inital value). In this case, we also specified STARTING_PAGE_INDEX . By default, Paging3 will load LOADSIZE*3 items during the first load. This way the user will see enough items when the list is loaded for the first time and it won’t trigger too many network requests unless the user scrolls through the page.

official guide

We also need to specify LoadResult as the return type. This class is a sealed class that holds the state of our request. If the request is successful, we can return the data as LoadResult.Page, and for subsequent requests, we can return the object by specifying the prevKey and nextKey. If there is a problem, we can return an error with LoadRestlt.Error.

The getRefreshKey abstract method that we need to override. The refresh key is used for subsequent refresh calls to PagingSource.load() (the first call is initial load which uses initialKey provided by Pager). A refresh happens whenever the Paging library wants to load new data to replace the current list, e.g., on swipe to refresh or on invalidation due to database updates, config changes, process death, etc. Typically, subsequent refresh calls will want to restart loading data centered around PagingState.anchorPosition which represents the most recently accessed index.

4.Repository

@Singleton
class UserRepositoryImpl @Inject constructor(
    private val userService: UserService
) : UserRepository {
    override fun getUsers(): Flow<PagingData<UserModel>> {
        return Pager(
            config = PagingConfig(
                pageSize = NETWORK_PAGE_SIZE
            ),
            pagingSourceFactory = { UserPagingDataSource(userService) }
        ).flow
    }


    companion object {
        const val NETWORK_PAGE_SIZE = 20
    }
}

Enter fullscreen mode Exit fullscreen mode

After writing the PagingSource class that will obtain the data, we need a Pager that will provide the data here as a flow. Data returned from PagingSource returns with the PagingData container. We need to specify to the Pager class from which source and how the data will be retrieved. It expects 3 parameters from us:

Image description

  • config : PagingConfigclass sets options regarding how to load content from a PagingSource such as how far ahead to load, the size request for the initial load, and others. The only mandatory parameter you have to define is the page size—how many items should be loaded in each page. By default, Paging will keep in memory all the pages you load. To ensure that you're not wasting memory as the user scrolls, set the maxSize parameter in PagingConfig. By default Paging will return null items as a placeholder for content that is not yet loaded if Paging can count the unloaded items and if the enablePlaceholders config flag is true.
  • initalKey: You can give an initial key for the first request to be made when the PagingSource is initial. In our example, since we do not specify an inital value, it will be defined as null. But on the PagingSource side, we handle the null state and give it the initial value.
  • pagingSourceFactory: A function that defines how to create the PagingSource. In our case, we’ll create a new UserPagingDataSource.

Finally, we can say .flow to provide the PagedData flow of the Pager class. Thus, as soon as the user reaches the end of the list, PagingSource will automatically and asynchronously send the request for the next page, and then the paginatedData will be presented to us as a flow each time.

Note: we have 4 type to pass the PagingData to other layers of our app:

  • Kotlin Flow - use Pager.flow.
  • LiveData - use Pager.liveData.
  • RxJava Flowable - use Pager.flowable.
  • RxJava Observable - use Pager.observable.

5.ViewModel

@HiltViewModel
class UserViewModel @Inject constructor(userRepository: UserRepository) : ViewModel() {
    val userItemsUiStates = userRepository.getUsers()
        .map { pagingData ->
            pagingData.map { userModel -> UserItemUiState(userModel) }
        }.cachedIn(viewModelScope)
}
Enter fullscreen mode Exit fullscreen mode

After successfully creating the side that will get the data, now let’s move on to the ui side.First, let’s request the users data from the repository on the viewModel side. The returned structure will be a PagingData and let’s map it to a required model class in every item we will use in the recylerview. Since we return to Flow, the Paging library provides us flexibility here as well, and we can perform operations such as filtering, mapping ..etc.

The cachedIn() operator makes the data stream shareable and caches the loaded data with the provided CoroutineScope. In any configuration change, it will provide the existing data instead of getting the data from scratch. It will also prevent memory leak.

6.Adapter

One of the components that Paging3 offers us is PagingDataAdapter. This Adapter is a special inherited class from RecyclerView.Adapter to show Paging Data based on it. It has a structure very similar to ListAdapter and adds it to the recylerview by calculating the new incoming list asynchronously on the background side with DiffUtil.ItemCallback in the most optimized way.

class UsersAdapter @Inject constructor() :
    PagingDataAdapter<UserItemUiState, UserViewHolder>(Comparator) {

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        getItem(position)?.let { userItemUiState -> holder.bind(userItemUiState) }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {

        val binding = inflate<ItemUserBinding>(
            LayoutInflater.from(parent.context),
            R.layout.item_user,
            parent,
            false
        )

        return UserViewHolder(binding)
    }

    object Comparator : DiffUtil.ItemCallback<UserItemUiState>() {
        override fun areItemsTheSame(oldItem: UserItemUiState, newItem: UserItemUiState): Boolean {
            return oldItem.getPhone() == newItem.getPhone()
        }

        override fun areContentsTheSame(
            oldItem: UserItemUiState,
            newItem: UserItemUiState
        ): Boolean {
            return oldItem == newItem
        }
    }

}

class UserViewHolder(private val binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root) {
    fun bind(userItemUiState: UserItemUiState) {
        binding.executeWithAction {
            this.userItemUiState = userItemUiState
        }
    }
}


Enter fullscreen mode Exit fullscreen mode
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="userItemUiState"
            type="com.huawei.pagingexampleproject.ui.UserItemUiState" />
    </data>

    <com.google.android.material.card.MaterialCardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="4dp"
        app:cardCornerRadius="4dp"
        app:cardElevation="4dp">


        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.imageview.ShapeableImageView
                android:id="@+id/ivPhoto"
                imageUrl="@{userItemUiState.imageUrl}"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:layout_gravity="center_horizontal"
                android:layout_marginBottom="8dp"
                android:adjustViewBounds="true"
                android:scaleType="centerCrop"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="@+id/glStart"
                app:layout_constraintTop_toTopOf="@+id/glTop"
                app:shapeAppearanceOverlay="@style/circle"
                app:srcCompat="@drawable/ic_launcher_background" />

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/tvName"
                style="@style/user_card_text_style"
                android:text="@{userItemUiState.name}"
                app:layout_constraintBottom_toTopOf="@+id/tvMail"
                app:layout_constraintEnd_toEndOf="@id/glEnd"
                app:layout_constraintStart_toEndOf="@id/ivPhoto"
                app:layout_constraintTop_toTopOf="@+id/ivPhoto"
                tools:text="Jhon Doe" />

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/tvMail"
                style="@style/user_card_text_style"
                android:text="@{userItemUiState.mail}"
                app:layout_constraintBottom_toTopOf="@+id/tvPhone"
                app:layout_constraintEnd_toEndOf="@id/glEnd"
                app:layout_constraintStart_toEndOf="@id/ivPhoto"
                app:layout_constraintTop_toBottomOf="@+id/tvName"
                tools:text="jon.doe@gmail.com" />

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/tvPhone"
                style="@style/user_card_text_style"
                android:text="@{userItemUiState.phone}"
                app:layout_constraintBottom_toBottomOf="@+id/ivPhoto"
                app:layout_constraintEnd_toEndOf="@id/glEnd"
                app:layout_constraintStart_toEndOf="@id/ivPhoto"
                app:layout_constraintTop_toBottomOf="@+id/tvMail"
                tools:text="0532 123 12 12" />


            <androidx.constraintlayout.widget.Guideline
                android:id="@+id/glStart"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintGuide_begin="8dp" />


            <androidx.constraintlayout.widget.Guideline
                android:id="@+id/glEnd"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintGuide_end="8dp" />

            <androidx.constraintlayout.widget.Guideline
                android:id="@+id/glTop"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                app:layout_constraintGuide_begin="8dp" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </com.google.android.material.card.MaterialCardView>
</layout>
Enter fullscreen mode Exit fullscreen mode
data class UserItemUiState(private val userModel: UserModel) : BaseUiState() {

    fun getImageUrl() = userModel.picture.thumbnail

    fun getName() = "${userModel.name.first} ${userModel.name.last}"

    fun getPhone() = userModel.phone

    fun getMail() = userModel.email

}
Enter fullscreen mode Exit fullscreen mode
fun <T : ViewDataBinding> T.executeWithAction(action: T.() -> Unit) {
    action()
    executePendingBindings()
}

Enter fullscreen mode Exit fullscreen mode

If we move to the Adapter part, we mapped our UserModel data coming to the viewmodel side as UserItemUiState for each item in the recylerview. Therefore, we specify the type of PagingData that will come to the Adapter as UserItemUiState. Comprator object for changes too. The executeWithAction function on the ViewHolder side is also an extension function defined to not write executePendingBindings every time. It’s essentially assigning variables for dataBinding.

7. Render Data

After preparing the adapter, it is time to send the incoming data to the adapter and complete the UI rendering.

@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
    private lateinit var binding: ActivityUserBinding
    private val viewModel: UserViewModel by viewModels()

    @Inject
    lateinit var userAdapter: UsersAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setBinding()
        setListener()
        setAdapter()
        collectLast(viewModel.userItemsUiStates, ::setUsers)
    }

    private fun setBinding() {
        binding = DataBindingUtil.setContentView(this, R.layout.activity_user)
    }

    private fun setListener() {
        binding.btnRetry.setOnClickListener { userAdapter.retry() }
    }


    private fun setAdapter() {
        collect(flow = userAdapter.loadStateFlow
            .distinctUntilChangedBy { it.source.refresh }
            .map { it.refresh },
            action = ::setUsersUiState
        )
        binding.rvUsers.adapter = userAdapter.withLoadStateFooter(FooterAdapter(userAdapter::retry))
    }

    private fun setUsersUiState(loadState: LoadState) {
        binding.executeWithAction {
            usersUiState = UsersUiState(loadState)
        }
    }

    private suspend fun setUsers(userItemsPagingData: PagingData<UserItemUiState>) {
        userAdapter.submitData(userItemsPagingData)
    }

}

Enter fullscreen mode Exit fullscreen mode

fun <T> LifecycleOwner.collectLast(flow: Flow<T>, action: suspend (T) -> Unit) {
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            flow.collectLatest(action)
        }
    }
}


fun <T> LifecycleOwner.collect(flow: Flow<T>, action: suspend (T) -> Unit) {
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            flow.collect {
                action.invoke(it)
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

First of all, we need to collect the flow that will come from the viewmodel. We have defined LifecycleOwner extensions to be lifecycle aware and reusable. You can get information about the concepts here for flow from this link and for repeatOnLifecyle from the link.

In order to handle the loading status of the data on the UI side, Paging library tracks the state of load requests for paged data and exposes it through the LoadState class. Your app can register a listener with the PagingDataAdapter to receive information about the current state and update the UI accordingly. These states are provided from the adapter because they are synchronous with the UI. This means that your listener receives updates when the page load has been applied to the UI.

LoadState can be in 3 states:

  • LoadState.NotLoading: If there is no active load operation and no error.
  • LoadState.Loading: If there is an active load operation.
  • LoadState.Error: If there is an error.

The loadStateFlow or addLoadStateListener() provided via PagingDataAdapter can be used to be aware of the loading status. These mechanisms provide access to a CombinedLoadStates object that includes information about the LoadState behavior for each load type.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="usersUiState"
            type="com.huawei.pagingexampleproject.ui.UsersUiState" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.UserActivity">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/topAppbar"
            style="@style/Widget.MaterialComponents.Toolbar.Primary"
            android:layout_width="0dp"
            android:layout_height="?attr/actionBarSize"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:title="@string/user_list" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rvUsers"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="@{usersUiState.listVisibility}"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/topAppbar"
            tools:listitem="@layout/item_user" />

        <ProgressBar
            android:id="@+id/progressBar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{usersUiState.progressBarVisibility}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/topAppbar" />


        <Button
            android:id="@+id/btnRetry"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/retry"
            android:visibility="@{usersUiState.errorVisibility}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/topAppbar" />

        <TextView
            android:id="@+id/tvError"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@{usersUiState.getErrorMessage(context)}"
            android:visibility="@{usersUiState.errorVisibility}"
            app:layout_constraintEnd_toEndOf="@+id/btnRetry"
            app:layout_constraintStart_toStartOf="@+id/btnRetry"
            app:layout_constraintTop_toBottomOf="@+id/btnRetry"
            tools:text="Internet Connection Failed" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Enter fullscreen mode Exit fullscreen mode
data class UsersUiState(
    private val loadState: LoadState
) : BaseUiState() {

    fun getProgressBarVisibility() = getViewVisibility(isVisible = loadState is LoadState.Loading)

    fun getListVisibility() = getViewVisibility(isVisible = loadState is LoadState.NotLoading)

    fun getErrorVisibility() = getViewVisibility(isVisible = loadState is LoadState.Error)

    fun getErrorMessage(context: Context) = if (loadState is LoadState.Error) {
        loadState.error.localizedMessage ?: context.getString(R.string.something_went_wrong)
    } else ""
}

Enter fullscreen mode Exit fullscreen mode

Let’s handle the loading, success or error situations by observing these changes in the ui part. First, we can use the source parameter that CombinedState gives us to listen to the paging source status. So we can handle the loading state when the first request is thrown. Let’s set the state we are listening through PagingDataAdapter with databinding over UsersUiState model class.

Another adapter component offered by the paging library is LoadStateAdapter. This adapter provides access to the current list’s loading state. When the user reaches the end of the list by using a custom ViewHolder, let’s take action according to the loading status. Here we can add it as a header or footer. We can add both together. You can find detailed information from the link.

class FooterAdapter(
    private val retry: () -> Unit
) : LoadStateAdapter<FooterViewHolder>() {
    override fun onBindViewHolder(holder: FooterViewHolder, loadState: LoadState) {
        holder.bind(loadState)
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        loadState: LoadState
    ): FooterViewHolder {
        val itemPagingFooterBinding = inflate<ItemPagingFooterBinding>(
            LayoutInflater.from(parent.context),
            R.layout.item_paging_footer,
            parent,
            false
        )
        return FooterViewHolder(itemPagingFooterBinding, retry)
    }

}


class FooterViewHolder(
    private val binding: ItemPagingFooterBinding,
    retry: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    init {
        binding.btnRetry.setOnClickListener { retry.invoke() }
    }

    fun bind(loadState: LoadState) {
        binding.executeWithAction {
            footerUiState = FooterUiState(loadState)
        }
    }
}

Enter fullscreen mode Exit fullscreen mode
data class FooterUiState(private val loadState: LoadState) : BaseUiState() {

    fun getLoadingVisibility() = getViewVisibility(isVisible = loadState is LoadState.Loading)

    fun getErrorVisibility() = getViewVisibility(isVisible = loadState is LoadState.Error)

    fun getErrorMessage(context: Context) = if (loadState is LoadState.Error) {
        loadState.error.localizedMessage ?: context.getString(R.string.something_went_wrong)
    } else ""
}

Enter fullscreen mode Exit fullscreen mode
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="footerUiState"
            type="com.huawei.pagingexampleproject.common.FooterUiState" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="10dp">

        <ProgressBar
            android:id="@+id/progressBar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:visibility="@{footerUiState.loadingVisibility}" />

        <Button
            android:id="@+id/btnRetry"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="@string/retry"
            android:visibility="@{footerUiState.errorVisibility}" />

        <TextView
            android:id="@+id/tvError"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="4dp"
            android:text="@{footerUiState.getErrorMessage(context)}"
            android:visibility="@{footerUiState.errorVisibility}"
            tools:text="Internet Connection Failed" />

    </LinearLayout>
</layout>
Enter fullscreen mode Exit fullscreen mode

After preparing this structure, we can set our recylerview adapter using the withLoadStateFooter() function.In fact, this structure returns us a ConcatAdapter. It notifies the LoadStateAdapter of the status of adding to the list via the PagingDataAdapter, and we can show the loading status of the newly added paginatedData when the user reaches the end of the list in a custom way via the FooterAdapter we have created. In addition, with the ConcatAdapter structure, we show multiviewtype on the recylerview.

Image description

withLoadStateFooter expects a parameter of type LoadStateAdapter from us. Here we give the FooterAdapter that we created ourselves.FooterAdapter also expects retry function in constructure. This structure is also provided through the pagingDataAdapter. If there is an error in loading when the end of the page is reached, the retry button will appear with the error message. It is enough to call the retry function over the pagingDataAdapater we created so that we can send the page request to be loaded again. This is again provided to us by the paging3 library.


Conclusion

In this article, I tried to explain the pagination method used when working with large data sets and Paging3, which is offered to android developers to easily implement this method. With the advantages it provides, Paging3 provides us with great convenience and enables us to show large data sets to the user in the most optimized way. The components of the Paging library are designed to fit into the recommended Android app architecture, integrate cleanly with other Jetpack components, and provide first-class Kotlin support. See you in the next article. 👋👋


To review the repo we wrote:

https://github.com/oguz-sahin/PagingExampleProject


https://developer.android.com/topic/libraries/architecture/paging/v3-overview

https://developer.android.com/codelabs/android-paging#

Top comments (0)