Cara Membuat Pagination atau Load More Menggunakan RecyclerView Part 2

Yudi Setiawan 5 November 2017

Cara Membuat Pagination atau Load More Menggunakan RecyclerView Part 2

Tutorial ini menggunakan bahasa pemrograman Kotlin

Pendahuluan

Pada tutorial sebelumnya Cara Membuat Pagination atau Load More Menggunakan RecyclerView Part 1 sudah saya bahas sedikit tentang pengenalan apa itu pagination dan tujuan dibuatnya fungsi pagination pada aplikasi. Jadi, pada tutorial lanjutan ini saya akan mengajak Anda untuk belajar cara membuat aplikasi di Android yang memanfaatkan fungsi pagination dengan menggunakan data dari TheMovieDb. Dimana, nantinya aplikasi yang akan kita buat akan menampilkan daftar film yang masuk dalam kategori now playing dan kemudian, ketika di-scroll sampai paling bawah maka, aplikasi-nya akan melakukan pengambilan data untuk halaman berikutnya.

Persiapan TheMovieDb

Pembuatan Akun TheMovieDb

Untuk pembuatan akunnya Anda bisa kunjungi situs TheMovieDb. Dan pilih Login jika sudah punya akun dan pilih Sign Up untuk melakukan pendaftaran akun.

Home Page TheMovieDb

Kemudian, lakukan pengisian data akun untuk Anda melakukan pendaftaran akun seperti berikut.

Sign Up TheMovieDb

Setelah itu, silakan login dan masuk ke menu settings dan pilih API

Menu API TheMovieDb

Dokumentasi Endpoint TheMovieDb

Untuk melihat dokumentasi endpoint TheMovieDb bisa Anda cari-cari sendiri di halaman website tersebut. Dan endpoint yang akan kita pakai pada tutorial ini adalah https://api.themoviedb.org/3/movie/now_playing?api_key={api_key}&language=en-us&page={page}

Mulai Pembuatan Projek

Buat Projek di Android Studio

Silakan buat projek baru di Android Studio dan isi dengan keterangan berikut. Application name: Codepolitan-Movie Company domain: codepolitan.com

Application name dan Company domain aplikasi

Selanjutnya, pilih Next dan pilih Minimum SDK ke API 15

Minimum SDK aplikasi

Setelah itu, di halaman berikutnya pilih Empty Activity

Empty Activity New Project

Dan dihalaman berikutnya, biarkan saja dan pilih Finish

Customize the Activity New Project

Setelah itu, tunggu beberapa saat sampai Android Studio selesai melakukan konfigurasi.

Configuration Create New Project

Konfigurasi Dependensi Gradle

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

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.2"
    defaultConfig {
        applicationId "com.codepolitan.codepolitan_movie"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    flavorDimensions "production", "development"
    productFlavors {
        production {
            applicationIdSuffix ".production"
            buildConfigField("String", "API_KEY", "\"Di isi dengan nilai API Key Anda\"") // Ex: \"d6dfdsfdsf\"
            buildConfigField("String", "BASE_URL", "\"https://api.themoviedb.org/3/\"")
            buildConfigField("String", "LANGUAGE", "\"EN-US\"")
            buildConfigField("String", "BASE_URL_IMAGE", "\"https://image.tmdb.org/t/p/w185/\"")
            dimension "production"
        }
        development {
            applicationIdSuffix ".development"
            buildConfigField("String", "API_KEY", "\"Di isi dengan nilai API Key Anda\"") // Ex: \"d6dfdsfdsf\"
            buildConfigField("String", "BASE_URL", "\"https://api.themoviedb.org/3/\"")
            buildConfigField("String", "LANGUAGE", "\"EN-US\"")
            buildConfigField("String", "BASE_URL_IMAGE", "\"https://image.tmdb.org/t/p/w185/\"")
            dimension "development"
        }
    }
}

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'
    })
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:design:26.1.0'
    implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    implementation 'com.squareup.retrofit2:retrofit-converters:2.3.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
    implementation 'io.reactivex.rxjava2:rxjava:2.1.6'
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
    implementation 'com.google.code.gson:gson:2.8.2'
    implementation 'com.github.bumptech.glide:glide:4.3.0'
}
repositories {
    mavenCentral()
    google()
}

Pada konfigurasi gradle diatas, ada beberapa hal yang konfigurasi yaitu sebagai berikut:

  1. Konfigurasi API KEY TheMovieDb di bagian productFlavors dimana, nilai ini harus Anda isi sesuai dengan nilai API Key (v3 auth) yang ada di menu API pada langkah sebelumnya.
  2. Konfigurasi BASE URL dari endpoint yang akan dipakai pada TheMovieDb dimana, nilainya kita set https://api.themoviedb.org/3/
  3. Konfigurasi LANGUAGE ini dipakai sebagai parameter ketika memanggil endpoint yang bersangkutan yakni, untuk memanggil endpoint https://api.themoviedb.org/3/movie/now_playing?api_key={api_key}&language=en-us&page={page} kita membutuhkan parameter LANGUAGE
  4. Konfigurasi BASE_URL_IMAGE ini dipakai untuk memanggil data poster dari TheMovieDb dimana, nilainya kita set https://image.tmdb.org/t/p/w185/
  5. Setelah, itu ada beberapa dependensi tambahan yang kita pakai pada projek kali ini yaitu, retrofit, rxjava dan beberapa dependensi tambahan lainnya.

Pembuatan layout activity_main.xml

Silakan buka file activity_main.xml dan isi dengan source code 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"
    tools:context="com.codepolitan.codepolitan_movie.MainActivity">

    <ProgressBar
        android:id="@+id/progress_bar_horizontal_activity_main"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:max="100" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view_movie_activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="8dp"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="8dp" />

</RelativeLayout>

Di file layout diatas, kita hanya membuat layout sederhana untuk menampilkan RecyclerView. Kira-kira seperti berikut ini tampilan dari layout diatas.

Layout activity_main.xml

Pembuatan layout item_movie.xml

Buat file layout baru dengan nama item_movie.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_movie"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <ImageView
        android:id="@+id/image_view_poster_item_movie"
        android:layout_width="150dp"
        android:layout_height="200dp"
        android:layout_marginEnd="@dimen/activity_horizontal_margin"
        android:layout_marginRight="@dimen/activity_horizontal_margin"
        android:contentDescription="@string/image_view_poster_movie"
        android:scaleType="fitXY" />

    <TextView
        android:id="@+id/text_view_title_movie_item_movie"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toEndOf="@+id/image_view_poster_item_movie"
        android:layout_toRightOf="@+id/image_view_poster_item_movie"
        android:text="@string/title_movie"
        android:textSize="20sp"
        android:textStyle="bold" />

    <ImageView
        android:id="@+id/image_view_vote_average_item_movie"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_view_title_movie_item_movie"
        android:layout_toEndOf="@+id/image_view_poster_item_movie"
        android:layout_toRightOf="@+id/image_view_poster_item_movie"
        android:contentDescription="@string/image_view_vote_average"
        android:src="@drawable/ic_star_pink_24dp" />

    <TextView
        android:id="@+id/text_view_vote_average_item_movie"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_view_title_movie_item_movie"
        android:layout_toEndOf="@+id/image_view_vote_average_item_movie"
        android:layout_toRightOf="@+id/image_view_vote_average_item_movie"
        android:text="@string/_0_0"
        android:textSize="15sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/text_view_release_date_item_movie"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_view_vote_average_item_movie"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:layout_toEndOf="@+id/image_view_poster_item_movie"
        android:layout_toRightOf="@+id/image_view_poster_item_movie"
        android:text="@string/release_date"
        android:textColor="@android:color/darker_gray" />

    <TextView
        android:id="@+id/text_view_release_date_value_item_movie"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_view_release_date_item_movie"
        android:layout_toEndOf="@+id/image_view_poster_item_movie"
        android:layout_toRightOf="@+id/image_view_poster_item_movie"
        android:text="@string/release_date_value" />

    <TextView
        android:id="@+id/text_view_overview_item_movie"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_view_release_date_value_item_movie"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:layout_toEndOf="@+id/image_view_poster_item_movie"
        android:layout_toRightOf="@+id/image_view_poster_item_movie"
        android:text="@string/overview"
        android:textColor="@android:color/darker_gray" />

    <TextView
        android:id="@+id/text_view_overview_value_item_movie"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_view_overview_item_movie"
        android:layout_toEndOf="@+id/image_view_poster_item_movie"
        android:layout_toRightOf="@+id/image_view_poster_item_movie"
        android:ellipsize="end"
        android:maxLines="3"
        android:text="@string/overview_value" />

</RelativeLayout>

Output dari file layout item_movie.xml ialah seperti berikut.

Layout item_movie

Pembuatan file model atau data class

Silakan Anda akses endpoint https://api.themoviedb.org/3/movie/now_playing?api_key=API Key Anda&language=EN-US&page=1 melalui aplikasi Postman atau sejenisnya dan kurang lebih seperti inilah data yang mereka berikan.

Data TheMovieDb

Kemudian, Anda copy and paste data tersebut ke Android Studio menggunakan plugin JSON To Kotlin Data Class dan jika sudah Anda buat maka, kurang lebih seperti inilah hasil dari file yang dibuatnya.

Data class TheMovieDb
Data class Result
Data class Dates

Pembuatan file ApiTheMovieDb

Selanjutnya, Anda buat file baru bernama ApiTheMovieDb dan isi dengan source code berikut

package com.codepolitan.codepolitan_movie.api

import com.codepolitan.codepolitan_movie.BuildConfig
import com.codepolitan.codepolitan_movie.model.TheMovieDb
import io.reactivex.Observable
import retrofit2.http.GET
import retrofit2.http.Query

/**
 * Created by yudisetiawan on 11/4/17.
 */

interface ApiTheMovieDb {

    @GET("movie/now_playing")
    fun getNowPlaying(
            @Query("api_key") apiKey: String = BuildConfig.API_KEY,
            @Query("language") language: String = BuildConfig.LANGUAGE,
            @Query("page") page: Int
    ): Observable<TheMovieDb>
}

Dimana, pada file ini Anda akan membuat sebuah interface untuk si Retrofit dan satu endpoint yang nantinya akan Anda panggil untuk mengambil data dari TheMovieDb.

Pembuatan file AdapterTheMovieDb

Buat file baru dengan nama AdapterTheMovieDb dan isi dengan source code berikut

package com.codepolitan.codepolitan_movie.adapter

import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bumptech.glide.Glide
import com.codepolitan.codepolitan_movie.BuildConfig
import com.codepolitan.codepolitan_movie.R
import com.codepolitan.codepolitan_movie.model.Result
import kotlinx.android.synthetic.main.item_movie.view.*

/**
 * Created by yudisetiawan on 11/4/17.
 */
class AdapterTheMovieDb(private val context: Context, private var resultTheMovieDb: ArrayList<Result>) : RecyclerView.Adapter<AdapterTheMovieDb.ViewHolderTheMovieDb>() {

    private val TAG = javaClass.simpleName

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolderTheMovieDb =
            ViewHolderTheMovieDb(LayoutInflater
                    .from(parent?.context)
                    .inflate(R.layout.item_movie, parent, false)
            )

    override fun onBindViewHolder(holder: ViewHolderTheMovieDb?, position: Int) {
        val resultItem = resultTheMovieDb[position]
        Glide
                .with(context)
                .load(BuildConfig.BASE_URL_IMAGE + resultItem.posterPath)
                .into(holder?.itemView?.image_view_poster_item_movie)
        holder
                ?.itemView
                ?.text_view_title_movie_item_movie
                ?.text = resultItem.originalTitle
        holder
                ?.itemView
                ?.text_view_vote_average_item_movie
                ?.text = resultItem.voteAverage.toString()
        holder
                ?.itemView
                ?.text_view_release_date_value_item_movie
                ?.text = resultItem.releaseDate
        holder
                ?.itemView
                ?.text_view_overview_value_item_movie
                ?.text = resultItem.overview
    }

    override fun getItemCount(): Int = resultTheMovieDb.size

    fun refreshAdapter(resultTheMovieDb: List<Result>) {
        this.resultTheMovieDb.addAll(resultTheMovieDb)
        notifyItemRangeChanged(0, this.resultTheMovieDb.size)
    }

    inner class ViewHolderTheMovieDb(itemView: View?) : RecyclerView.ViewHolder(itemView)

}

Pada file tersebut Anda ada mendeklarasikan inner class ViewHolder dengan nama class ViewHolderTheMovieDb. Selanjutnya, pada file AdapterTheMovieDb Anda juga membuat sebuah primary constructor dimana, ada 2 parameter yang diberikan yaitu, Context dan ArrayList<Result>. Selanjutnya, pada fun onBindViewHolder Anda ada menggunakan Glide untuk me-load gambar dari TheMovieDb pada syntax berikut.

        Glide
                .with(context)
                .load(BuildConfig.BASE_URL_IMAGE + resultItem.posterPath)
                .into(holder?.itemView?.image_view_poster_item_movie)

Setelah, itu Anda juga ada meng-set text pada text_view_title_movie_item_movie, text_view_vote_average_item_movie, text_view_release_date_value_item_movie, text_view_overview_value_item_movie pada syntax berikut.

        holder
                ?.itemView
                ?.text_view_title_movie_item_movie
                ?.text = resultItem.originalTitle
        holder
                ?.itemView
                ?.text_view_vote_average_item_movie
                ?.text = resultItem.voteAverage.toString()
        holder
                ?.itemView
                ?.text_view_release_date_value_item_movie
                ?.text = resultItem.releaseDate
        holder
                ?.itemView
                ?.text_view_overview_value_item_movie
                ?.text = resultItem.overview

Selain itu, Anda juga membuat fun refreshAdapter dimana, function ini berfungsi untuk me-refresh adapter si RecyclerView

MainActivity

Yang terakhir, silakan buka file MainActivity dan isi dengan source code berikut.

package com.codepolitan.codepolitan_movie

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.Log
import android.view.View
import com.codepolitan.codepolitan_movie.adapter.AdapterTheMovieDb
import com.codepolitan.codepolitan_movie.api.ApiTheMovieDb
import com.codepolitan.codepolitan_movie.model.TheMovieDb
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_main.*
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import kotlin.properties.Delegates

class MainActivity : AppCompatActivity() {

    private val TAG = javaClass.simpleName
    private var adapterTheMovieDb by Delegates.notNull<AdapterTheMovieDb>()
    private var isLoading by Delegates.notNull<Boolean>()
    private var page by Delegates.notNull<Int>()
    private var totalPage by Delegates.notNull<Int>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        page = 1
        totalPage = 0
        doLoadData()
        initListener()
    }

    private fun doLoadData() {
        Log.d(TAG, "page: $page")
        showLoading(true)
        val retrofit = Retrofit.Builder()
                .baseUrl(BuildConfig.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build()
        val apiTheMovieDb = retrofit.create(ApiTheMovieDb::class.java)
        apiTheMovieDb.getNowPlaying(page = page)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(
                        { theMovieDb: TheMovieDb ->
                            val resultTheMovieDb = theMovieDb.results as ArrayList
                            if (page == 1) {
                                adapterTheMovieDb = AdapterTheMovieDb(
                                        this@MainActivity,
                                        resultTheMovieDb
                                )
                                recycler_view_movie_activity_main.layoutManager = LinearLayoutManager(this@MainActivity)
                                recycler_view_movie_activity_main.adapter = adapterTheMovieDb
                            } else {
                                adapterTheMovieDb.refreshAdapter(resultTheMovieDb)
                            }
                            totalPage = theMovieDb.totalPages
                        },
                        { t: Throwable ->
                            t.printStackTrace()
                        },
                        {
                            hideLoading()
                        }
                )
    }

    private fun initListener() {
        recycler_view_movie_activity_main.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
                val linearLayoutManager = recyclerView?.layoutManager as LinearLayoutManager
                val countItem = linearLayoutManager?.itemCount
                val lastVisiblePosition = linearLayoutManager?.findLastCompletelyVisibleItemPosition()
                val isLastPosition = countItem.minus(1) == lastVisiblePosition
                Log.d(TAG, "countItem: $countItem")
                Log.d(TAG, "lastVisiblePosition: $lastVisiblePosition")
                Log.d(TAG, "isLastPosition: $isLastPosition")
                if (!isLoading && isLastPosition && page < totalPage) {
                    showLoading(true)
                    page = page.let { it.plus(1) }
                    doLoadData()
                }
            }
        })
    }

    private fun showLoading(isRefresh: Boolean) {
        isLoading = true
        progress_bar_horizontal_activity_main.visibility = View.VISIBLE
        recycler_view_movie_activity_main.visibility.let {
            if (isRefresh) {
                View.VISIBLE
            } else {
                View.GONE
            }
        }
    }

    private fun hideLoading() {
        isLoading = false
        progress_bar_horizontal_activity_main.visibility = View.GONE
        recycler_view_movie_activity_main.visibility = View.VISIBLE
    }

}

Pada file ini, Anda ada mendeklarasikan 2 buah variable page dan totalPage dimana, kedua variable ini berperan untuk memberikan indikasi pada aplikasi apakah data yang di-load sudah masih memiliki halaman berikutnya atau tidak. Pada fun doLoadData Anda melakukan pengambilan data dari endpoint yang sudah Anda buat di file interface ApiTheMovieDb dimana, return value-nya itu Observable jadi, bisa Anda padukan dengan RxJava dan kemudian, Anda subscribe pada callback onNext untuk set adapter RecyclerView jika variable page == 1 dan jika tidak maka, refresh adapter RecyclerView. Selain itu, Anda juga ada mendeklarasikan listener RecyclerView.OnScrollListener dimana, listener ini berfungsi untuk si RecyclerView melakukan event tertentu ketika si RecyclerView di-scroll. Pada listener ini Anda memberikan kondisi jika data masih di-load dan scroll sudah berada di akhir item dan variable page < totalPage maka, fun doLoadData akan dijalankan lagi untuk mengambil data pada halaman berikutnya.

Struktur Project

Berikut adalah struktur project pada tutorial ini.

Struktur Project

Output Program

Berikut adalah output dari project yang sudah Anda kerjakan.

Output Program

Projek pada tutorial ini sudah saya upload ke Github