Cara Membuat Pagination atau Load More Menggunakan RecyclerView Part 1

Yudi Setiawan 26 September 2017

Cara Membuat Pagination atau Load More Menggunakan RecyclerView Part 1

Pengenalan

Pagination merupakan salah satu teknik dalam menampilkan sejumlah data yang banyak di aplikasi. Alasan utama mengapa sebuah aplikasi menampilkan data dalam bentuk pagination adalah untuk menghindari proses load data yang lama sehingga pengguna mengalami bad experience terhadap aplikasi yang kita buat. Dan berikut merupakan beberapa aplikasi yang menggunakan teknik pagination.

Tech in Asia ID
JIRA

Seperti yang Anda lihat pada kedua gambar diatas bahwa, kedua aplikasi tersebut menggunakan teknik pagination untuk menampilkan data hanya saja style mereka yang berbeda. Jadi, walaupun style-nya berbeda tapi, inti dari kedua gambar diatas ialah bahwa mereka menampilkan datanya menggunakan pagination.

Baca juga: Belajar Membuat Aplikasi Android dari 10 Aplikasi Open Source Ini

Analisa User Interface

Sebagai bahan latihan, mari kita analisa pada gambar aplikasi Tech In Asia ID. Berdasarkan analisa penulis, ada beberapa komponen yang dipakai yaitu sebagai berikut.

  1. RecyclerView
  2. Item RecyclerView Content
  3. Item RecyclerView Loading

Jadi, sebenarnya tekniknya ialah menggunakan multiple view type di RecyclerView. Bagi teman-teman yang belum tahu apa itu multiple view type silakan baca di sini. Jadi, ada 2 item yang mereka pakai di RecyclerView yaitu, item untuk menampilkan data dan item untuk menampilkan proses loading.

Mulai Pembuatan Projek Latihan

Buat Projek

Silakan buat projek baru di Android Studio dan setup default ke Empty Activity.

Konfigurasi build.gradle

Tambahkan dependency com.android.support:design:25.3.1 dan org.jetbrains.anko:anko-commons:0.10.1. Note: harap untuk versi 25.3.1 dan 0.10.1 ini disesuaikan dengan versi-nya masing-masing. Jadi, isi file build.gradle(app) menjadi seperti berikut. Dan jangan lupa untu di-sync.

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 25
    buildToolsVersion "26.0.1"
    defaultConfig {
        applicationId "com.ysn.codepolitan_pagination"
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    compile 'com.android.support:design:25.3.1'
    compile 'org.jetbrains.anko:anko-commons:0.10.1'
}

repositories {
    mavenCentral()
}

activity_main.xml

Buka file activity_main.xml dan ubah menjadi seperti berikut.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.ysn.codepolitan_pagination.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view_data_activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

Jadi, pada layout diatas kita cuma menampilkan si RecyclerView

item_data_content.xml

Buat file layout baru dengan item_data_content.xml dan isi dengan source code berikut.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/relative_layout_container_item_data_content"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <TextView
        android:id="@+id/text_view_number_item_data_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true" />

</RelativeLayout>

Pada layout item_data_content.xml kita cuma tampilkan TextView yang mana nanti kita akan pakai untuk menampilkan value index-nya

item_data_loading.xml

Buat file layout baru dengan nama item_data_loading.xml dan isi dengan source code berikut.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/relative_layout_container_item_data_loading"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <ProgressBar
        android:id="@+id/progress_bar_item_data_loading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true" />

</RelativeLayout>

Pada layout item_data_loading.xml kita hanya menampilkan ProgressBar untuk menampilkan proses loading

AdapterData.kt

Silakan buat file class baru dengan nama AdapterData.kt dan isi dengan source code berikut.

class AdapterData(private var listData: List<String>, private var listViewType: List<Int>) : RecyclerView.Adapter<AdapterData.ViewHolder>() {

    private val TAG = javaClass.simpleName
    companion object {
        val ITEM_VIEW_TYPE_CONTENT = 1
        val ITEM_VIEW_TYPE_LOADING = 2
    }

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
        val layoutInflater = LayoutInflater.from(parent?.context)
        return when (viewType) {
            ITEM_VIEW_TYPE_CONTENT -> ViewHolderContent(
                    layoutInflater.inflate(R.layout.item_data_content, null)
            )
            else -> ViewHolderLoading(
                    layoutInflater.inflate(R.layout.item_data_loading, null)
            )
        }
    }

    override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
        val viewType = listViewType[position]
        val data = listData[position]
        when (viewType) {
            ITEM_VIEW_TYPE_CONTENT -> {
                holder?.itemView
                        ?.text_view_number_item_data_content
                        ?.text = data
            }
            else -> {
                /** nothing to do in here */
            }
        }
    }

    override fun getItemCount(): Int = listData.size

    override fun getItemViewType(position: Int): Int = listViewType[position]

    fun refresh(listData: ArrayList<String>, listViewType: ArrayList<Int>) {
        Log.d(TAG, "refresh")
        this.listData = listData
        this.listViewType = listViewType
        notifyDataSetChanged()
    }

    open class ViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView)

    inner class ViewHolderContent(itemView: View?) : ViewHolder(itemView)

    inner class ViewHolderLoading(itemView: View?) : ViewHolder(itemView)

}

Jadi, pada file class AdapterData.kt akan kita pakai untuk adapter si RecyclerView yang mana pada adapter tersebut kita memakai 2 view type yaitu, view type untuk content dan view type untuk loading. Di method onBindViewHolder kita tidak memasang logika apapun hanya mengambil nilai dari objek listData dan kemudian menampilkannya pada text_view_number_item_data_content untuk view type content dan untuk view type loading itu tidak dipasang logika apapun karena, di view type loading kita cuma menampilkan ProgressBar doang. Kemudian, pada method getItemCount kita me-return-kan value dari listData.size. Selanjutnya, pada method getItemViewType kita juga me-return-kan value dari setiap item listViewType yang mana kemungkinan nilainya itu cuma 2 yaitu, ITEM_VIEW_TYPE_CONTENT dan ITEM_VIEW_TYPE_LOADING. Selanjutnya, pada method refresh itu kita buat berfungsi untuk meng-update data listData dan listViewType ketika fungsi load more berhasil di-execute

MainActivity.kt

Selanjutnya, buka file class MainActivity.kt dan ubah isinya sehingga menjadi seperti berikut

class MainActivity : AppCompatActivity() {

    private val TAG = javaClass.simpleName
    lateinit var listData: ArrayList<String>
    lateinit var listViewType: ArrayList<Int>
    var countLoadMore by Delegates.notNull<Int>()
    var isLoading by Delegates.notNull<Boolean>()

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

        isLoading = false
        listData = ArrayList()
        listViewType = ArrayList()
        countLoadMore = 0
        repeat(30) { a ->
            listData.add(a.toString())
            listViewType.add(AdapterData.ITEM_VIEW_TYPE_CONTENT)
        }       

        val adapterData = AdapterData(
                listData = listData,
                listViewType = listViewType
        )
        val linearLayoutManager = LinearLayoutManager(this)
        recycler_view_data_activity_main.layoutManager = linearLayoutManager
        recycler_view_data_activity_main.adapter = adapterData

        recycler_view_data_activity_main.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
                val countItem = linearLayoutManager.itemCount
                val lastVisiblePosition = linearLayoutManager.findLastCompletelyVisibleItemPosition()
                val isLastPosition = countItem.minus(1) == lastVisiblePosition
                if (!isLoading && isLastPosition && countLoadMore < 3) {
                    listData.add("")
                    listViewType.add(AdapterData.ITEM_VIEW_TYPE_LOADING)
                    adapterData.refresh(listData, listViewType)

                    isLoading = true
                    doAsync {
                        val lenTemp = listData.size - 1
                        repeat(10) { a ->
                            val lastValue: Int = when (a) {
                                0 -> {
                                    listData[listData.size - 2].toInt()
                                }
                                else -> {
                                    listData[listData.size - 1].toInt()
                                }
                            }
                            listData.add(lastValue.plus(1).toString())
                            listViewType.add(AdapterData.ITEM_VIEW_TYPE_CONTENT)
                        }
                        Thread.sleep(1000 * 5)
                        uiThread {
                            listData.removeAt(lenTemp)
                            listViewType.removeAt(lenTemp)
                            adapterData.refresh(listData, listViewType)
                            countLoadMore += 1
                            isLoading = false
                        }
                    }
                }
            }
        })
        
    }

}

Jadi, pada kode diatas kita ada membuat variable countLoadMore dan isLoading yang mana variable countLoadMore berfungsi untuk membatasi apakah dia sudah menjalankan fungsi load more berapa kali. Yang namanya pagination pasti ada halaman terakhirnya kan jadi, pada sample sederhana ini kita perlu membatasinya sebanyak 3 kali saja fungsi load more-nya berjalan.

Kemudian, variable isLoading berfungsi sebagai indikator apakah proses load more sedang berjalan atau tidak. Karena, apabila fungsi load more sedang berjalan dan kemudian, kita jalankan lagi maka, app akan tidak berjalan dengan benar. Jadi, fungsi load more hanya dijalankan sekali saja kemudian, tunggu hasilnya apakah fungsi load more-nya berhasil atau tidak. Ketika fungsi load more-nya selesai maka, variable isLoading akan kembali bernilai false.

Baca juga: Cara Membuat RecyclerView dengan Multiple View Type di Android

Selanjutnya, kita ada memberikan listener onScrolled pada RecyclerView dimana, didalamnya ada pengkondisian if apabila !isLoading && isLastPosition && countLoadMore < 3. Jadi, maksud pengkondisian ini adalah agar fungsi load more tadi hanya berjalan sekali saja. Jadi, bisa saja pengguna itu sudah scrolling sampai kebawah dan fungsi load more jalan kemudian, belum selesai fungsi load more berjalan tiba-tiba si user scroll ke atas lagi dan scroll ke bawah lagi. Jadi, pengkondisian if ini berfungsi untuk meng-handle scenario tersebut. Nah, didalam if tersebut ada kode

listData.add("")
listViewType.add(AdapterData.ITEM_VIEW_TYPE_LOADING)

yang mana, kode tersebut berfungsi untuk menambahkan item loading ketika fungsi load more mau dijalankan. Kemudian, setelah kode itu ada keyword doAsync yang mana syntax ini memiliki fungsi yang sama dengan AsyncTask di Java Android. Di Kotlin, saya pakai doAsync milik si Anko dan keyword uiThread ini merupakan method onPostExecute apabila di AsyncTask. Jadi, ketika kode Thread.sleep(1000 * 5) selesai maka, UI app langsung ter-update (data RecyclerView berubah). Sekarang coba tes projek Anda dan lihat hasilnya.

Apakah bisa??? Kalau tidak bisa jangan khawatir. Sekarang coba buka logcat pada Android Studio dan lihat apakah ada error atau warning message. Saya yakin pasti ada pesan warning-nya seperti ini kan.

Cannot call this method in a scroll callback. Scroll callbacks mightbe run during a measure & layout pass where you cannot change theRecyclerView data. Any method call that might change the structureof the RecyclerView or the adapter contents should be postponed tothe next frame.
                                                                              java.lang.IllegalStateException: 
                                                                                  at android.support.v7.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:2581)
                                                                                  at android.support.v7.widget.RecyclerView$RecyclerViewDataObserver.onChanged(RecyclerView.java:4932)
                                                                                  at android.support.v7.widget.RecyclerView$AdapterDataObservable.notifyChanged(RecyclerView.java:11359)
                                                                                  at android.support.v7.widget.RecyclerView$Adapter.notifyDataSetChanged(RecyclerView.java:6636)
                                                                                  at com.ysn.codepolitan_pagination.AdapterData.refresh(AdapterData.kt:57)
                                                                                  at com.ysn.codepolitan_pagination.MainActivity$onCreate$2.onScrolled(MainActivity.kt:53)
                                                                                  at android.support.v7.widget.RecyclerView.dispatchOnScrolled(RecyclerView.java:4618)
                                                                                  at android.support.v7.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:4776)
                                                                                  at android.view.Choreographer$CallbackRecord.run(Choreographer.java:873)
                                                                                  at android.view.Choreographer.doCallbacks(Choreographer.java:685)
                                                                                  at android.view.Choreographer.doFrame(Choreographer.java:618)
                                                                                  at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:859)
                                                                                  at android.os.Handler.handleCallback(Handler.java:751)
                                                                                  at android.os.Handler.dispatchMessage(Handler.java:95)
                                                                                  at android.os.Looper.loop(Looper.java:154)
                                                                                  at android.app.ActivityThread.main(ActivityThread.java:6195)
                                                                                  at java.lang.reflect.Method.invoke(Native Method)
                                                                                  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:874)
                                                                                  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:764)

Jika iya berarti, Anda telah mengikuti tutorial ini dengan benar. Sekarang mari kita analisa pesan warning-nya. Dia bilang bahwa di method callback scroll itu tidak boleh ada perubahan data pada RecyclerView dan apabila kita klik salah satu file yang ditunjuknya maka, akan mengarahkan kita pada kode berikut

adapterData.refresh(listData, listViewType)

Jadi, solusinya gimana??? Sekarang silakan Anda ubah kode pada file class MainActivity.kt menjadi seperti berikut.

class MainActivity : AppCompatActivity() {

    private val TAG = javaClass.simpleName
    lateinit var listData: ArrayList<String>
    lateinit var listViewType: ArrayList<Int>
    var countLoadMore by Delegates.notNull<Int>()
    var isLoading by Delegates.notNull<Boolean>()

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

        isLoading = false
        listData = ArrayList()
        listViewType = ArrayList()
        countLoadMore = 0
        repeat(30) { a ->
            listData.add(a.toString())
            listViewType.add(AdapterData.ITEM_VIEW_TYPE_CONTENT)
        }
        listData.add("")
        listViewType.add(AdapterData.ITEM_VIEW_TYPE_LOADING)

        val adapterData = AdapterData(
                listData = listData,
                listViewType = listViewType
        )
        val linearLayoutManager = LinearLayoutManager(this)
        recycler_view_data_activity_main.layoutManager = linearLayoutManager
        recycler_view_data_activity_main.adapter = adapterData

        /*recycler_view_data_activity_main.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
                val countItem = linearLayoutManager.itemCount
                val lastVisiblePosition = linearLayoutManager.findLastCompletelyVisibleItemPosition()
                val isLastPosition = countItem.minus(1) == lastVisiblePosition
                if (!isLoading && isLastPosition && countLoadMore < 3) {
                    listData.add("")
                    listViewType.add(AdapterData.ITEM_VIEW_TYPE_LOADING)
                    adapterData.refresh(listData, listViewType)

                    isLoading = true
                    doAsync {
                        val lenTemp = listData.size - 1
                        repeat(10) { a ->
                            val lastValue: Int = when (a) {
                                0 -> {
                                    listData[listData.size - 2].toInt()
                                }
                                else -> {
                                    listData[listData.size - 1].toInt()
                                }
                            }
                            listData.add(lastValue.plus(1).toString())
                            listViewType.add(AdapterData.ITEM_VIEW_TYPE_CONTENT)
                        }
                        Thread.sleep(1000 * 5)
                        uiThread {
                            listData.removeAt(lenTemp)
                            listViewType.removeAt(lenTemp)
                            adapterData.refresh(listData, listViewType)
                            countLoadMore += 1
                            isLoading = false
                        }
                    }
                }
            }
        })*/

        recycler_view_data_activity_main.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
                val countItem = linearLayoutManager.itemCount
                val lastVisiblePosition = linearLayoutManager.findLastCompletelyVisibleItemPosition()
                val isLastPosition = countItem.minus(1) == lastVisiblePosition
                Log.d(TAG, "isLoading: $isLoading & isLastPosition: $isLastPosition & countLoadMore: $countLoadMore")
                if (!isLoading && isLastPosition && countLoadMore < 3) {
                    isLoading = true
                    doAsync {
                        val lenTemp = listData.size - 1
                        repeat(10) { a ->
                            val lastValue: Int = when (a) {
                                0 -> {
                                    listData[listData.size - 2].toInt()
                                }
                                else -> {
                                    listData[listData.size - 1].toInt()
                                }
                            }
                            listData.add(lastValue.plus(1).toString())
                            listViewType.add(AdapterData.ITEM_VIEW_TYPE_CONTENT)
                        }
                        Thread.sleep(1000 * 10)
                        uiThread {
                            listData.removeAt(lenTemp)
                            listViewType.removeAt(lenTemp)

                            if (countLoadMore + 1 < 3) {
                                listData.add("")
                                listViewType.add(AdapterData.ITEM_VIEW_TYPE_LOADING)
                            }
                            adapterData.refresh(listData, listViewType)
                            countLoadMore += 1
                            isLoading = false
                        }
                    }
                }
            }
        })

    }

}

Sekarang coba Anda lihat baik-baik dimanakah perbedaannya. Yap, perbedaannya adalah terletak pada action untuk menambahkan item loading-nya. Jadi, ketika looping repeat itu kita langsung tambahkan item loading-nya. Sekarang coba jalankan lagi dan lihat hasilnya apakah sudah benar atau tidak.

Output

Output Load More Berjalan
Output Selesai Load More

Oya, untuk part-2 nanti saya akan buatkan contoh aplikasi-nya yang benar-benar menggunakan data real dari API ya. Projek pada tutorial ini sudah saya upload ke Github ya.