0
0
0
share
#Android#RecyclerView#kotlin#loadmore
0 Komentar
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.


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.
- RecyclerView
- Item RecyclerView Content
- 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


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.
0
0
0
share