1. What is Paging3

Paging3 is a library developed by Google for Android. Paging3 allows applications to seamlessly load and display data retrieved locally or from the server.

Compared to Paging2, Paging3 has the following 5 updates.

  1. PagingSourceThe implementation of the.
  2. supportFlow.
  3. Added callback to state when requesting data,
  4. Allows you to setHeaderandFooter.
  5. Supports multiple ways to request data, such as network requests and database requests.

A tutorial on Paging2 can be found here: juejin.cn/post/684490…

2. Architecture of Paging3

The diagram above shows the architecture of the application using Paging3. The data is retrieved from the Repository layer and passed to the Pager in the ViewModel layer. Finally, PagingDataAdaper retrieves the Flow data from the ViewModel and displays it.

  • PagingSource: is a single data source, PagingSource can load data locally or from a server.
  • RemoteMediator: is also a single data source that uses RemoteMediator data when there is no data in the PagingSource. If the data exists in both the database and the server, more often PagingSource is used for database requests and RemoteMediator is used for server requests.
  • PagingData: container for single-page data.
  • Pager: a class that builds Flow to perform a callback when data is loaded.
  • PagingDataAdapter: Adapter for paging load data RecyclerView.

3. Implementation of Paging3

3.1 import library

Add library dependencies to the Gradle file for the current project as needed.

    def paging_version = "3.0.0 - beta01"

    implementation "androidx.paging:paging-runtime:$paging_version"
    
    // alternatively - without Android dependencies for tests
    testImplementation "androidx.paging:paging-common:$paging_version"

    // optional - RxJava2 support
    implementation "androidx.paging:paging-rxjava2:$paging_version"

    // optional - RxJava3 support
    implementation "androidx.paging:paging-rxjava3:$paging_version"

    // optional - Guava ListenableFuture support
    implementation "androidx.paging:paging-guava:$paging_version"

    // Jetpack Compose Integration
    implementation "Androidx. The paging: the paging - compose: 1.0.0 - alpha08"
Copy the code

3.2 configuration DataStore

There are three types of Page sources in Paging2, which leads us to think carefully about which one to use before using it. This has developers scratching their heads. However, there is only one Page Source in the Paging3, so there is no need to worry about which one to choose. Just use it with your eyes closed. The Page Source needs to inherit from the PagingSource, and the load and getRefreshKey methods need to be overridden.

class PersonDataSource(private val repository: PersonRepository) : PagingSource<Int, Person>() {
    // Load data when got data from DataSource(server or local cache).
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Person> {
        valpos = params.key ? : START_INDEXval startIndex = pos * params.loadSize + 1
        val endIndex = (pos + 1) * params.loadSize
        return try {
            // Get data from DataSource
            val personList = repository.getPersonList(startIndex, endIndex)
            // Return data to RecyclerView by LoadResult
            LoadResult.Page(
                personList,
                if (pos <= START_INDEX) null else pos - 1.if (personList.isEmpty()) null else pos + 1)}catch (exception: Exception) {
            // Return exception by LoadResult
            LoadResult.Error(exception)
        }
    }

    // Return the position of data when refresh RecyclerView.
    override fun getRefreshKey(state: PagingState<Int, Person>): Int? {
        return 0
    }

    companion object {
        private const val START_INDEX = 0}}Copy the code

As can be seen from the above code, information about the loadSize and key (start location) of Paging can be obtained with the LoadParams parameter.

3.3 Create an observable data set

Next we need to create an observable data set in the ViewModel. In this case, Observable data sets refer to data types such as LiveData,Flow, and Observable and Flowable in RxJava. It is worth noting that if you are using RxJava data sets, don’t forget to introduce RxJava libraries.

In my Demo I used Flow.

    var personList =
        Pager(
            config = PagingConfig(
                pageSize = 20,
                enablePlaceholders = false,
                initialLoadSize = 20
            ),
            pagingSourceFactory = {
                PersonDataSource(repository)
            }
        ).flow
Copy the code

Pass PagingConfig and PagingSource to the Pager, which returns a Flow. In PagingConfig, we need to set PageSize, enablePlaceholders and initialLoadSize. In PagingSourceFactor, pass in the DataSource we already wrote above.

3.4 Creating an Adapter

We need to create a RecyclerView adapter, which needs to inherit from PagingDataAdapter. The notation is similar to ListAdapter.

class PersonAdapter(private val context: Context) :
    PagingDataAdapter<Person, PersonAdapter.ViewHolder>(PersonDiffCallback()) {

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        valperson = getItem(position) holder.binding.also { it.textViewName.text = person? .name it.textViewAge.text = person?.age.toString()if (position % 2= =0) { Glide.with(context).load(person? .photoUrl).into(it.imageView) }else {
                Glide.with(context).load(R.drawable.ic_studio_icon).into(it.imageView)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            RecyclerItemBinding.inflate(
                LayoutInflater.from(context), parent, false))}class ViewHolder(val binding: RecyclerItemBinding) :
        RecyclerView.ViewHolder(binding.root)
}

class PersonDiffCallback : DiffUtil.ItemCallback<Person>() {
    override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
        return oldItem.name == newItem.name
    }

    override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
        return oldItem == newItem
    }
}
Copy the code

3.5 according to the item

The implementation method here is the same as the general RecyclerView display Item. In short, you need to achieve the following.

  1. Create and set up an instance of adapter.
  2. Create coroutines.
  3. Receive Flow data in Coroutines scope.
        val adapter = PersonAdapter(this)
        binding.recyclerView.adapter = adapter

        // launch an coroutines, and received flow data. Finally, submit data to the adapter.
        lifecycleScope.launch {
            viewModel.personList.collectLatest {
                adapter.submitData(it)
            }
        }
Copy the code

3.6 Observing the Loaded Data Status

We can set up a listener in the Adapter to observe the loading status of the data. There are three states.

  1. LoadState.Loading: The application is loading data.
  2. LoadState.NotLoading: The application is not loading data.
  3. LoadState.Error: The application failed to load data.
        adapter.addLoadStateListener {state ->
            when(state.refresh){
                is LoadState.Loading -> {
                    binding.swipeRefreshLayout.isRefreshing = true
                }
                is LoadState.NotLoading -> {
                    binding.swipeRefreshLayout.isRefreshing = false
                }
                is LoadState.Error -> {
                    // show an error dialog.}}}Copy the code

3.7 Setting Header and Footer

We can display a particular View when the data is loaded and when more is loaded. First, we need to create an Adapter that inherits from the LoadStateAdapter. The implementation is similar to that of the PagingDataAdapter, except for the onBindViewHolder method. Refer to the code below for details.

class PersonLoadStateAdapter(private val context: Context) :
    LoadStateAdapter<PersonLoadStateAdapter.ViewHolder>() {

    override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
        // change the view when LoadStat was changed.
        when(loadState){
            is LoadState.Loading -> {
                holder.binding.progressBar.visibility = View.VISIBLE
                holder.binding.errorMsg.visibility = View.INVISIBLE
                holder.binding.retryButton.visibility = View.INVISIBLE
            }
            is LoadState.NotLoading -> {
                holder.binding.progressBar.visibility = View.INVISIBLE
                holder.binding.errorMsg.visibility = View.INVISIBLE
                holder.binding.retryButton.visibility = View.INVISIBLE
            }
            is LoadState.Error -> {
                holder.binding.progressBar.visibility = View.INVISIBLE
                holder.binding.errorMsg.visibility = View.VISIBLE
                holder.binding.retryButton.visibility = View.VISIBLE
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
        return ViewHolder(
            LoadStateFooterViewItemBinding.inflate(
                LayoutInflater.from(context), parent, false))}class ViewHolder(val binding: LoadStateFooterViewItemBinding) :
        RecyclerView.ViewHolder(binding.root)
}
Copy the code

The second step, we can get on LoadStateAdapter joining together to PagingDataAdapter withLoadStateHeaderAndFooter method. Then pass the Adapter to RecyclerView after stitching.

        val adapter = PersonAdapter(this)

        val concatAdapter: ConcatAdapter = adapter.withLoadStateFooter(
            footer = PersonLoadStateAdapter(this)
        )

        binding.recyclerView.adapter = concatAdapter
Copy the code

4. Others



Github: Github.com/HyejeanMOON…

Easy to use Jetpack Compose tutorial

1. Compose’s programming idea

Jetpack tutorials

Jetpack Paging3 tutorial 2. Jetpack DataStore Tutorial 3. Android Jetpack Room Tutorial 4 The use of the WorkManager

Other tutorial

Dagger Hilt!! Scoped Storage for Android Scoped Storage (Scoped) for Android Scoped Storage (Scoped) Use of Google’s MergeAdapter