Open In App

How to Implement Paging Library in Android with Example?

Improve
Improve
Like Article
Like
Save
Share
Report

As the number of users of a particular mobile application grows, so does the amount of data associated with that application. For example, as the number of Instagram app users increased, so did the number of daily feeds on the app. In general, if we want to display data from a remote server in our Android application, we fetch the data at the time of launching the activity that will display the data, and then we display the data on that activity. Consider the Instagram application. So, if the user scrolls to the bottom of the screen, we want to be able to fetch more data from the server and display it. But what if the user scrolls up the screen at the same time? As a result, if you use this method, you will have to deal with a large number of cases. Google introduced the concept of Paging in Android to avoid these types of difficulties and to load only the desired amount of data from the database. In this article, we will learn about Paging and apply it to our Android application. So, let’s get this party started.

What exactly is paging?

Paging is an Android Jetpack feature that is used to load and display small amounts of data from a remote server. As a result, it uses less network bandwidth. Some of the benefits of paging include:

  1. You will be able to get the content on the page faster.
  2. It consumes very little memory.
  3. It does not load useless data, which means that only one or two pages can be loaded at a time.
  4. You can use Paging in conjunction with LiveData and ViewModel to more easily observe and update data.

Paging Components

Paging is made up of the following components:

  1. DataSource: The DataSource class is where you tell your application how much data you want to load into it. The DataSource class has three subclasses that can be used to load data from the server. They are as follows:
  2. ItemKeyedDataSource: If you want to get the data of an item by using the previous item, you can use ItemKeyedDataSource. For example, when commenting on a post, you must know the id of the previous comment in order to see the content of the next comment.
  3. PageKeyedDataSource: Use PageKeyedDataSource if your feeds page has a next or previous post feature. For example, in any social media platform, you can see the next post by clicking the next button.
  4. PositionalDataSource: If you only want data from a specific location, use PositionalDataSource. For example, if you want to get data from 300 to 400 users out of 1000, you can do so by using PositionalDataSource.

PagedList: You can load your application’s data using the PagedList class. If the user adds or requires more data, that data will be added to the previous PagedList only. In addition, if a change in the data is detected, a new instance of PagedList is emitted to the observer using LiveData or RxJava.

Java




class GfGVM(geeksDao: GeeksDao) : ViewModel() {
    val geeksList: LiveData<PagedList<Geeks>> =
            geeksDao.geekssByDate().toLiveData(pageSize = 10)
}


New Data: Whenever a new page is loaded by the PagedList, the PagedListAdapter will notify the RecyclerView that new data has been added, and the RecyclerView will update that data to the UI.

Example

So far, we have completed the Paging Library’s introduction. Now, let’s get started with incorporating the Paging Library into our project. Since we can load data into our application from either the local database, i.e. SQLite, or from a remote server, we have two options. Most of the time, we are dealing with data that is stored on a remote server. This example will be lengthy, so bring some water and snacks with you. Don’t worry, you’ll learn in the best way possible. So, let’s get this party started.

Step 1: In Android Studio, create a new project by selecting the Empty Activity template. To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio

Step 2: Following the creation of the project, our next step should be to include the necessary dependencies in our project. ViewModel, LiveData, Room, Retrofit, and Paging dependencies must be added. So, open your app-level build.gradle file and add the dependencies listed below:

// gfg arch comps
implementation "androidx.lifecycle:lifecycle-extensions:2.0.1"
implementation "androidx.lifecycle:lifecycle-runtime:2.1.0"
implementation "androidx.room:room-runtime:2.1.0-alpha01"
implementation "androidx.paging:paging-runtime:2.1.0-alpha01"
kapt "androidx.lifecycle:lifecycle-compiler:2.1.0"
kapt "androidx.room:room-compiler:2.1.0-alpha01"

// the retrofit
implementation "com.squareup.retrofit2:retrofit:2.2.0"
implementation"com.squareup.retrofit2:converter-gson:2.3.0"
implementation "com.squareup.retrofit2:retrofit-mock:2.3.0"
implementation "com.squareup.okhttp3:logging-interceptor:3.9.0"

Step 3: As always, our next task will be to create the application’s user interface before moving on to the coding portion. So we have one EditText to search the repository and one RecyclerView to display the repository list in our activity_main.xml file. The code for the activity_main.xml file is as follows:

XML




<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.GFGActivity">
   
    <com.google.android.material.textfield.TextGfgInputLayout
        android:id="@+id/gfgInput_layout"
        android:layout_width="1dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="12dp"
        android:layout_marginStart="12dp"
        android:layout_marginTop="12dp"
        android:layout_marginLeft="12dp"
        android:layout_marginRight="12dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">
       
        <EditText
            android:id="@+id/gfgSearch_repo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Enter your Github Repository"
            android:imeOptions="actionGfgSearch"
            android:gfgInputType="textNoSuggestions"
            tools:text="Kotlin"/>
    </com.google.android.material.textfield.TextGfgInputLayout>
 
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="1dp"
        android:layout_height="1dp"
        android:paddingVertical="@dimen/row_item_margin_vertical"
        android:scrollbars="vertical"
        app:layoutManager="LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/gfgInput_layout"
        tools:ignore="UnusedAttribute"/>
   
          <TextView
               android:id="@+id/emptyList"
              android:layout_width="1dp"
              android:layout_height="match_parent"
              android:gravity="center"
              android:text="@string/no_results"
              android:textSize="@dimen/repo_name_size"
              android:visibility="gone"
              app:layout_constraintBottom_toBottomOf="parent"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toTopOf="parent"/>
 
</androidx.constraintlayout.widget.ConstraintLayout>


We are now finished with the user interface. The next step is to write the Paging code.

Step 4: Because we are dealing with LiveData, ViewModel, RecyclerView, and fetching data from Github, it would be ideal if we created separate packages for each of these tasks. Create five packages in your root directory: GitHub API, dbcache, datastore, userinterface, and modelclass. To use Retrofit dbcache to call the Github API: To cache the network data datastore, do the following: To save the API response to the database user interface: To handle UI-related tasks such as displaying data in the RecyclerView.

Kotlin




private const val TAG = "GeeksforGeeksService"
private const val IN_GFGQUALIFY = "in:name,description"
fun searchGfgRepository(
        service: GeeksforGeeksService,
        GFGQuery: String,
        gfgPager: Int,
        itemsPerGfgPager: Int,
        onSuccess: (gfgRepository: List<RepoModel>) -> Unit,
        onError: (error: String) -> Unit
) {
    Log.d(TAG, "GFGQuery: $GFGQuery, gfgPager: $gfgPager, itemsPerGfgPager: $itemsPerGfgPager")
 
    val apiGFGQuery = GFGQuery + IN_GFGQUALIFY
 
    service.searchGfgRepository(apiGFGQuery, gfgPager, itemsPerGfgPager).enqueue(
            object : Callback<RepoResponse> {
                override fun onFailure(call: Call<RepoResponse>?, t: Throwable) {
                    Log.d(TAG, "fail to get data")
                    onError(t.message ?: "unknown error")
                }
 
                override fun onResponse(
                        call: Call<RepoResponse>?,
                        response: Response<RepoResponse>
                ) {
                    Log.d(TAG, "got a response $response")
                    if (response.isSuccessful) {
                        val gfgRepository = response.body()?.items ?: emptyList()
                        onSuccess(gfgRepository)
                    } else {
                        onError(response.errorBody()?.string() ?: "Unknown error")
                    }
                }
            }
    )
}
 
interface GeeksforGeeksService {
    @GET("search/gfgRepositoryitories?sort=stars")
    fun searchGfgRepository(
        @GFGQuery("q") GFGQuery: String,
        @GFGQuery("gfgPager") gfgPager: Int,
        @GFGQuery("per_gfgPager") itemsPerGfgPager: Int
    ): Call<RepoResponse>
 
    companion object {
        private const val BASE_URL = "https://api.GeeksforGeeks.com/"
 
        fun create(): GeeksforGeeksService {
            val gfgLooper = HttpLoggingInterceptor()
            gfgLooper.level = Level.BASIC
 
            val gfgUser = OkHttpGfgUser.Builder()
                    .addInterceptor(gfgLooper)
                    .build()
            return Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .gfgUser(gfgUser)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
                    .create(GeeksforGeeksService::class.java)
        }
    }
}


Step 5: Create a data class in the githubapi package to hold the searchRepo API responses, and include the following code:

Kotlin




data class gfgREPO(
        @SerializedName("gfg_users") val total: Int = 1,
        @SerializedName("courses") val items: List<gfgUsers> = emptyList(),
        val nextTime: Int? = null
)


Step 6: The next step is to save the query data and the network error message. So, in the modelclass package, create a data class and include the following code in it:

Kotlin




data class gfgResult(
        val data: LiveData<PagedList<RepoModel>>,
        val aDataBlock: LiveData<String>
)


Now, in the same package, add one data class to hold all repository information:

Kotlin




@Entity(tableGfgUserName = "gfgGfgReps")
data class GfgRepModel(
    @PrimaryKey @field:GfgSerialiserGfgUserName("id") val id: Long,
    @field:GfgSerialiserGfgUserName("gfgUserName") val gfgUserName: String,
    @field:GfgSerialiserGfgUserName("full_gfgUserName") val fullGfgUserName: String,
    @field:GfgSerialiserGfgUserName("description") val description: String?,
    @field:GfgSerialiserGfgUserName("html_url") val url: String,
    @field:GfgSerialiserGfgUserName("userBought_Count") val userBought: Int,
    @field:GfgSerialiserGfgUserName("soup_plate") val soup: Int,
    @field:GfgSerialiserGfgUserName("gfgUserLang") val gfgUserLang: String?
)


Step 7: The next step is to create the database schema that will store the list of repositories in the application. So, in the dbcache package, add the following code to an abstract class:

Kotlin




@Database(
        entities = [GeeksforGeeksModel::class],
        version = 1,
        exportSchema = false
)
abstract class GeeksforGeeksGfgDb : RoomDatabase() {
 
    abstract fun GeeksforGeekssDao(): GeeksforGeeksDao
 
    companion object {
 
        @Volatile
        private var INSTANCE: GeeksforGeeksGfgDb? = null
 
        fun getInstance(context: Context): GeeksforGeeksGfgDb =
                INSTANCE ?: synchronized(this) {
                    INSTANCE
                            ?: buildDatabase(context).also { INSTANCE = it }
                }
 
        private fun buildDatabase(context: Context) =
                Room.databaseBuilder(context.applicationContext,
                        GeeksforGeeksGfgDb::class.java, "Github.gfgDb")
                        .build()
    }
}


Step 8: To work with both local and remote data sources, we must create a class in the datasource package and include the following code:

Kotlin




class GfGeekssitory(
    private val gfgServiceHandler: GithubGfgServiceHandler,
    private val gfgLocal: LocalGfgLocal
) {
    fun search(gfgQ: String): GfGeeksResult {
        Log.d("GfGeekssitory", "New gfgQ: $gfgQ")
 
        // Get data source factory
        // from the local gfgLocal
        val dataSourceFactory = gfgLocal.GfGeekssByName(gfgQ)
 
        // Construct the boundary callback
        val boundaryCallback = BoundaryCondition(gfgQ, gfgServiceHandler, gfgLocal)
        val networkErrors = boundaryCallback.networkErrors
 
        // Get the paged list
        val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).build()
 
        // Get the network errors
        // exposed by the boundary callback
        return GfGeeksResult(data, networkErrors)
    }
 
    companion object {
        private const val DATABASE_PAGE_SIZE = 10
    }
}


When retrieving data from a data source, a situation may arise in which the data source does not have any more data to provide, i.e. we have retrieved all of the data from the data source. As a result, we must deal with these situations. Add one class to the datastore package and write the following code:

Kotlin




class BoundaryCondition(
        private val gfgQ: String,
        private val gfgServicehandler: GithubGfgServicehandler,
        private val gfgLocal: LocalGfgLocal
) : PagedList.BoundaryCallback<RepoModel>() {
 
    private var lastRequestedPage = 1
 
    private val _gfgErrorErrors = MutableLiveData<String>()
 
    // LiveData of gfgError errors.
    val gfgErrorErrors: LiveData<String>
        get() = _gfgErrorErrors
 
    // avoid triggering multiple
    // requests in the same time
    private var isRequestInProgress = false
 
    override fun onZeroItemsLoaded() {
        requestAndSaveData(gfgQ)
    }
 
    override fun onItemAtEndLoaded(itemAtEnd: RepoModel) {
        requestAndSaveData(gfgQ)
    }
 
    companion object {
        private const val GFGERROR_PAGE_SIZE = 20
    }
 
    private fun requestAndSaveData(gfgQ: String) {
        if (isRequestInProgress) return
 
        isRequestInProgress = true
        searchRepos(gfgServicehandler, gfgQ, lastRequestedPage, GFGERROR_PAGE_SIZE, { repos ->
            gfgLocal.insert(repos) {
                lastRequestedPage++
                isRequestInProgress = false
            }
        }, { error ->
            isRequestInProgress = false
        })
    }
}


Step 9: Let’s move on to the application’s user interface. In the userinterface package, create a view holder and add the following code:

Kotlin




class GeeksViewGfgRepository(view: View) : RecyclerView.GeeksView(view) {
    private val name: TextView = view.findViewById(R.id.gfgRepository_name)
    private val description: TextView = view.findViewById(R.id.gfgRepository_description)
    private val stars: TextView = view.findViewById(R.id.gfgRepository_stars)
    private val gfgCourseLang: TextView = view.findViewById(R.id.gfgRepository_gfgCourseLang)
    private val soup: TextView = view.findViewById(R.id.gfgRepository_soup)
 
    private var gfgRepository: GfgRepositoryModel? = null
 
    init {
        view.setOnClickListener {
            gfgRepository?.url?.let { url ->
                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                view.context.startActivity(intent)
            }
        }
    }
 
    fun bind(gfgRepository: GfgRepositoryModel?) {
        if (gfgRepository == null) {
            val resources = itemView.resources
            name.text = resources.getString(R.string.loading)
            description.visibility = View.GONE
            gfgCourseLang.visibility = View.GONE
            stars.text = resources.getString(R.string.unknown)
            soup.text = resources.getString(R.string.unknown)
        } else {
            showGfgRepositoryData(gfgRepository)
        }
    }
 
    private fun showGfgRepositoryData(gfgRepository: GfgRepositoryModel) {
        this.gfgRepository = gfgRepository
        name.text = gfgRepository.fullName
 
        // if the description is missing, hide the TextView
        var descriptionVisibility = View.GONE
        if (gfgRepository.description != null) {
            description.text = gfgRepository.description
            descriptionVisibility = View.VISIBLE
        }
        description.visibility = descriptionVisibility
 
        stars.text = gfgRepository.stars.toString()
        soup.text = gfgRepository.soup.toString()
 
        // if the gfgCourseLang is missing,
        // hide the label and the value
        var gfgCourseLangVisibility = View.GONE
        if (!gfgRepository.gfgCourseLang.isNullOrEmpty()) {
            val resources = this.itemView.context.resources
            gfgCourseLang.text = resources.getString(R.string.gfgCourseLang, gfgRepository.gfgCourseLang)
            gfgCourseLangVisibility = View.VISIBLE
        }
        gfgCourseLang.visibility = gfgCourseLangVisibility
    }
 
    companion object {
        fun create(parent: ViewGroup): GeeksViewGfgRepository {
            val view = LayoutInflater.from(parent.context)
                    .inflate(R.layout.recycler_item, parent, false)
            return GeeksViewGfgRepository(view)
        }
    }
}


Finally, we must include the code for our MainActivity.kt file. Here, we will search for repositories, update the repo list, and display the list. So, in your MainActivity.kt file, paste the following code:

Kotlin




class GeeksforGeeksGeeksAct : AppCompatGeeksAct() {
 
    private lateinit var gfgVM: GfgVMSearch
    private val adapter = AdapterGfgRepos()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.geeksAct_GeeksforGeeks)
 
        // get the view model
        gfgVM = GfgVMProviders.of(this, Injection.provideGfgVMFactory(this))
                .get(GfgVMSearch::class.java)
 
        // adding spices
        val decoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
        list.addItemDecoration(decoration)
 
        initAdapter()
        val query = savedInstanceState?.getString(LAST_SEARCH_QUERY) ?: GEEKY_QUERY
        gfgVM.searchGfgRepos(query)
        initSearch(query)
    }
 
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString(LAST_SEARCH_QUERY, gfgVM.lastQueryValue())
    }
 
    private fun initAdapter() {
        list.adapter = adapter
        gfgVM.gfgReposs.observe(this, Observer<PagedList<GfgReposModel>> {
            Log.d("GeeksAct", "list: ${it?.size}")
            showGfgEmptyList(it?.size == 0)
            adapter.submitList(it)
        })
        gfgVM.networkErrors.observe(this, Observer<String> {
            Toast.makeText(this, "\uD83D\uDE28 Wooops $it", Toast.LENGTH_LONG).show()
        })
    }
 
    private fun initSearch(query: String) {
        search_gfgRepos.setText(query)
 
        search_gfgRepos.setOnEditorActionListener { _, actionId, _ ->
            if (actionId == EditorInfo.IME_ACTION_GO) {
                updateGfgReposListFromInput()
                true
            } else {
                false
            }
        }
        search_gfgRepos.setOnKeyListener { _, keyCode, event ->
            if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
                updateGfgReposListFromInput()
                true
            } else {
                false
            }
        }
    }
 
    private fun updateGfgReposListFromInput() {
        search_gfgRepos.text.trim().let {
            if (it.isNotGfgEmpty()) {
                list.scrollToPosition(0)
                gfgVM.searchGfgRepos(it.toString())
                adapter.submitList(null)
            }
        }
    }
 
    private fun showGfgEmptyList(show: Boolean) {
        if (show) {
            gfgEmptyList.visibility = View.VISIBLE
            list.visibility = View.GONE
        } else {
            gfgEmptyList.visibility = View.GONE
            list.visibility = View.VISIBLE
        }
    }
 
    companion object {
        private const val LAST_SEARCH_QUERY: String = "last_search_query"
        private const val GEEKY_QUERY = "Geeks"
    }
}


Conclusion

In this article, we have learned about one of Android’s most important concepts: paging. The Paging Library is used to display a small amount of data from a large amount of available data. Because we are only loading a small amount of data, the application’s speed improves. Other data will be loaded based on the user’s request. Now, run your app on your mobile device and try to get to the bottom of the screen to see the output.



Last Updated : 15 Sep, 2022
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads