Jetpack Paging, which provides a list Paging loading solution for Android, has recently released its latest 3.0-alpha version. Paging3 is rewritten based on the Kotlin coroutine and is compatible with Flow, RxJava, LiveData and other forms of API.

This article takes a look at the basic use of Paging3 through an example OF an API request. We use the mock interface provided by https://reqres.in/ :

request reqres.in/api/users? P…
response

The Sample code structure is as follows:

Step 01. Gradle dependencies


First add the Gradle dependency for Paging3

implementation "androidx.paging:paging-runtime:{latest-version}"
Copy the code

Retrofit and MoSHI are used for data request and deserialization in sample

implementation "com.squareup.retrofit2:retrofit:{latest-version}"
implementation "com.squareup.retrofit2:converter-moshi:{latest-version}"
implementation "com.squareup.moshi:moshi-kotlin:{latest-version}" 
Copy the code

MVVM is implemented using the ViewModel

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:{latest-version}"
Copy the code

Step 02. ApiService & Retrifit


Define APIService and Retrofit instances:

interface APIService {

    @GET("api/users")
    suspend fun getListData(@Query("page") pageNumber: Int): Response<ApiResponse>

    companion object {
        
        private val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()

        fun getApiService(a) = Retrofit.Builder()
            .baseUrl("https://reqres.in/")
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()
            .create(APIService::class.java)
    }
}
Copy the code

Define the suspend function getListData to make paging requests asynchronously in a coroutine; Use Moshi as the deserialization tool

Step 03. Data Structure


Create a JSON IDL based on the result returned by the API:

{
    "page": 1."per_page": 6."total": 12."total_pages": 2."data": [{"id": 1."email": "[email protected]"."first_name": "George"."last_name": "Bluth"."avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/calebogden/128.jpg"
    },
    {
        "id": 2."email": "[email protected]"."first_name": "Janet"."last_name": "Weaver"."avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg"}]."ad": {
    "company": "StatusCode Weekly"."url": "http://statuscode.org/"."text": "A weekly newsletter focusing on software development, infrastructure, the server, performance, and the stack end of things."
}}
Copy the code

Define a data class based on JSON

data class ApiResponse(
    @Json(name = "ad")
    val ad: Ad,
    @Json(name = "data")
    val myData: List<Data>,
    @Json(name = "page")
    val page: Int.@Json(name = "per_page")
    val perPage: Int.@Json(name = "total")
    val total: Int.@Json(name = "total_pages")
    val totalPages: Int
)

data class Ad(
    @Json(name = "company")
    val company: String,
    @Json(name = "text")
    val text: String,
    @Json(name = "url")
    val url: String
)

data class Data(
    @Json(name = "avatar")
    val avatar: String,
    @Json(name = "email")
    val email: String,
    @Json(name = "first_name")
    val firstName: String,
    @Json(name = "id")
    val id: Int.@Json(name = "last_name")
    val lastName: String
)
Copy the code

Step 04. PagingSource


Implementation of PagingSource interface, rewrite suspend method, through APIService API request

class PostDataSource(private val apiService: APIService) : PagingSource<Int, Data>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {
       
    }

}
Copy the code

The two generic parameters for PagingSource are Int, which represents the page of the current request, and Data, the Data type of the request

Suspend function load implementation:

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {
    try {
        valcurrentLoadingPageKey = params.key ? :1
        val response = apiService.getListData(currentLoadingPageKey)
        val responseData = mutableListOf<Data>()
        val data= response.body()? .myData ? : emptyList() responseData.addAll(data)

        val prevKey = if (currentLoadingPageKey == 1) null else currentLoadingPageKey - 1

        return LoadResult.Page(
            data = responseData,
            prevKey = prevKey,
            nextKey = currentLoadingPageKey.plus(1))}catch (e: Exception) {
        return LoadResult.Error(e)
    }
}
Copy the code

If param.key is empty, page 1 data is loaded by default. The successful request returns paging data using loadResult. Page, with prevKey and nextKey representing the index of the previous and next pages, respectively. Failed request returns Error status with loadResult. Error

Step 05. ViewModel


Define the ViewModel and create the Pager instance in the ViewModel

class MainViewModel(private val apiService: APIService) : ViewModel() {
	val listData = Pager(PagingConfig(pageSize = 6)) {
   	 	PostDataSource(apiService)
	}.flow.cachedIn(viewModelScope)
}
Copy the code
  • PagingConfigUsed to configure Pager,pageSizeRepresents the number of items loaded per page. This size is generally recommended to exceed the number of items displayed on a screen.
  • .flowIndicates that the result changes from LiveData to Flow
  • cachedInCaches the result toviewModelScope, will remain in the ViewModel until onClear

Step 06. Activity


Define the Activity and create the ViewModel

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
Copy the code
class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel
    lateinit var mainListAdapter: MainListAdapter

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setupViewModel()
        setupList()
        setupView()
    }

    private fun setupView(a) {
        lifecycleScope.launch {
            viewModel.listData.collect {
                mainListAdapter.submitData(it)
            }
        }
    }

    private fun setupList(a) {
        mainListAdapter = MainListAdapter()
        recyclerView.apply {
            layoutManager = LinearLayoutManager(this)
            adapter = mainListAdapter
        }
    }

    private fun setupViewModel(a) {
        viewModel =
            ViewModelProvider(
                this,
             MainViewModelFactory(APIService.getApiService())
            )[MainViewModel::class.java]
    }
}
Copy the code

The ApiService needs to be passed in the ViewModel, so you need to customize the ViewModelFactory

class MainViewModelFactory(private val apiService: APIService) : ViewModelProvider.Factory {

    override fun 
        create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(apiService) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")}}Copy the code

Create recyclerView. Adapter in setupList, setupView load data through viewModel.listData and submit recyclerView. Adapter for display.

Step 07. PagingDataAdapter


Update the MainListAdapter to inherit the PagingDataAdapter, which binds Data to the ViewHolder

class MainListAdapter : PagingDataAdapter<Data, MainListAdapter.ViewHolder>(DataDifferntiator) {

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view)

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.itemView.textViewName.text =
            "${getItem(position)? .firstName} ${getItem(position)? .lastName}"holder.itemView.textViewEmail.text = getItem(position)? .email }override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            LayoutInflater
                .from(parent.context)
                .inflate(R.layout.list_item, parent, false))}object DataDifferntiator : DiffUtil.ItemCallback<Data>() {

        override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
            return oldItem.id == newItem.id
        }

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

}
Copy the code

Paging receives a DiffUtil Callback to handle the diff of an Item, where a DataDifferntiator is specified for the construction of a PagingDataAdapter.

The ViewHolder layout is defined as follows:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="4dp">

    <TextView
        android:id="@+id/textViewName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:layout_marginBottom="4dp"
        android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large" />

    <TextView
        android:id="@+id/textViewEmail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:layout_marginBottom="4dp" />
</LinearLayout>
Copy the code

Now that we can display paginated data in the Activity, we’ll optimize the display further.

Step 08. LoadingState


You can use addLoadStateListener to display the loading state when a list is loaded

mainListAdapter.addLoadStateListener {
    if (it.refresh == LoadState.Loading) {
        // show progress view
    } else {
        //hide progress view}}Copy the code

Step 09. Header & Footer


Create a HeaderFooterAdapter that inherits from the LoadStateAdapter. OnBindViewHolder can return LoadState

class HeaderFooterAdapter() : LoadStateAdapter<HeaderFooterAdapter.ViewHolder>() {

    override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {

        if (loadState == LoadState.Loading) {
            //show progress viewe
        } else //hide the view
       
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
        return LoadStateViewHolder(
           //layout file)}class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view)
}
Copy the code

Through withLoadStateHeaderAndFooter add it to the MainListAdapter

mainListAdapter.withLoadStateHeaderAndFooter(
    header = HeaderFooterAdapter(),
    footer = HeaderFooterAdapter()
)
Copy the code

You can also set hader or footer separately

// only footer
mainListAdapter.withLoadStateFooter(
    HeaderFooterAdapter()
}

// only header
mainListAdapter.withLoadStateHeader(
   HeaderFooterAdapter()
)
Copy the code

Others: RxJava


If you’re not comfortable with Coroutine or Flow, Page 3 also supports RxJava

Add RxJava dependencies

implementation "androidx.paging:paging-rxjava2:{latest-version}"
implementation "com.squareup.retrofit2:adapter-rxjava2:{latest-version}"
Copy the code

Use RxPagingSource instead of PagingSource

class PostDataSource(private val apiService: APIService) : RxPagingSource<Int, Data>() {

}
Copy the code

Load instead returns the normal method of the Single interface

override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, Data>> {

}
Copy the code

The ApiService interface has also been changed to Single

interface APIService {

    @GET("api/users")
     fun getListData(@Query("page") pageNumber: Int): Single<ApiResponse>

    companion object {

        private val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()

        fun getApiService(a) = Retrofit.Builder()
            .baseUrl("https://reqres.in/")
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()
            .create(APIService::class.java)
    }
}
Copy the code

To configure Pager, convert LiveData to RxJava using.Observable

val listData = Pager(PagingConfig(pageSize = 6)) {
    PostDataSource(apiService)
}.observable.cachedIn(viewModelScope)
Copy the code

Request data in the Activity

viewModel.listData.subscribe { 
    mainListAdapter.submitData(lifecycle,it)
}
Copy the code

Conclusion


Paging3 has been rewritten based on Coroutine to recommend Flow as the preferred method for data requests, although it also retains support for RxJava. Although it is still the alpha version, there should be little change in API. Students who want to try Paging in the project should upgrade to the latest version as soon as possible