Android : Fetching Data with Retrofit also with Jetpack
Apa itu fetch data ? Fetch merupakan fungsi dasar untuk meminta sumber daya melalui jaringan, Secara dasar berhubungan dengan request & response(permintaan/tanggapan). Saya ambil di postnya bapak Rin disini wkwk. Kali ini kita akan belajar melakukan fetch data di Android dengan component Android Jetpack. Yang belum tahu Android Jetpack silahkan baca dulu postingan saya sebelum ini. Pertama yang perlu kita persiapkan adalah sebuah project, disini saya akan membuat sebuah project yang menampilkan list film populer. Saya mengambil data dari sebuah website film yang bernama TMDB. Anunya kira-kira seperti ini :
Kita pakai arsitektur MVVM (Model View ViewModel). Model adalah representasi data yang digunakan biasanya berupa POJO di Java atau data classes di Kotlin. View adalah UI dari sebuah app yang bertugas menampilkan data. Sedangkan ViewModel tempat nyimpen Logic UI dan berinteraksi langsung ke View. Oke lanjut.
Kita buat dulu project di Android Studio, kemudian di build.gradle tambahin beberapa library dibawah, jangan lupa enable databinding :
// Navigation
implementation "android.arch.navigation:navigation-fragment-ktx:$version_navigation"
implementation "android.arch.navigation:navigation-ui-ktx:$version_navigation"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:$version_lifecycle_extensions"
implementation "com.squareup.retrofit2:retrofit: $version_retrofit"
implementation "com.squareup.retrofit2:converter-moshi:$version_retrofit"
implementation "com.squareup.moshi:moshi:$version_moshi"
implementation "com.squareup.moshi:moshi-kotlin:$version_moshi"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version_kotlin_coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version_kotlin_coroutines"
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$version_retrofit_coroutines_adapter"
implementation "com.github.bumptech.glide:glide:$version_glide"
implementation "androidx.recyclerview:recyclerview:$version_recyclerview"
dataBinding {
enabled = true
}
Lifecycle Component
Didalam lifecycle component ada LiveData yang akan kita gunakan untuk melakukan observe data si Modelnya jadi ketika terjadi perubahan data maka akan langsung di update ke View nya.
Retrofit dan Moshi
Retrofit digunakan untuk melakukan request data ke API. Sedangkan Moshi digunakan untuk melakukan parse data dari JSON ke Java atau Kotlin Object.
Lanjut dulu aja deh. *wkwk
Buat model sesuai dengan response apinya. Disini saya menggunakan Movie.kt dan MovieResponse.kt
data class Movie(
val id: Int,
val title: String,
@Json(name="poster_path")
val posterPath: String,
val overview: String
)
data class MovieResponse(
val page: Int,
@Json(name="total_results")
val totaResults: Int,
@Json(name="total_pages")
val totalPages: Int,
val results: MutableList<Movie>
)
Buat file dengan nama Apiservice.kt isinya adalah :
Base Url dari Api kita
private const val BASE_URL = "https://api.themoviedb.org/3/"
inisialiasasi Moshi dan Retrofit
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(BASE_URL)
.build()
pada .addCallAdapterFactory kita ubah metode Call dari retrofit dengan memakai Coroutine Call. Kita akan memakai Deferred dari Coroutine karena di dalamnya terdapat metode .await() yang berfungsi mengeksekusi Call secara Asyncronously istilah lainnya non-blocking UI dimana metode tersebut tidak akan mengganggu thread UI kita. Lanjut masih di file yang sama tambahkan kode dibawah.
interface Apiservice {
@GET("movie/popular")
fun getMovies(
@Query("api_key") type: String
) : Deferred<MovieResponse>
}
Fungsi tersebut akan melakukan call sesuai dengan endpoint yang diberikan. Dibawahnya lagi kita buat sebuah object yang menginisialisasi Apiservice ke retrofit.
object Api {
val retrofitService : Apiservice by lazy {
retrofit.create(Apiservice::class.java)
}
Pada layout activity_main, kita coba buat desain seperti ini :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent" android:layout_height="wrap_content"
android:text="Popular Movies"
android:textColor="@color/colorPrimary"
android:textSize="20sp"
android:layout_marginTop="8dp"
android:padding="8dp"/>
<fragment
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"/>
</LinearLayout>
Setelah itu kita buat fragment untuk membuat UI halaman depannya disini saya beri nama HomeFragment juga membuat ViewModel bernama HomeViewModel. Pada layout fragment_home kita tambahkan RecylerView untuk menampilkan List data.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewModel" type="com.example.lambo.home.HomeViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/movie_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="16"
android:padding="6dp"
tools:listitem="@layout/grid_view_item"
tools:layout_editor_absoluteX="8dp" tools:layout_editor_absoluteY="107dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Kita pindah ke HomeViewModel. Buat HomeViewModel extends sebagai ViewModel kemudian kita buat variabel LiveData yang berfungsi sebagai Observer
private val _movies = MutableLiveData<List<Movie>>()
val movies: LiveData<List<Movie>> get() = _movies
Lalu kita buat viewModelJob dan init ke Coroutine Scope
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main)
Pastikan untuk mematikan Job ketika ViewModel sudah tidak digunakan lagi, dengan kata lain pada method onCleared(). Kemudian kita buat init getMovies() untuk melakukan fetch data ketika ViewModel pertama kali dijalankan.
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
init {
getMovies()
}
Selanjutnya pada method getMovies() tambahkan kode ini :
coroutineScope.launch {
val getMoviesDeferred = Api.retrofitService.getMovies(API_KEY)
try {
val listResult = getMoviesDeferred.await()
_movies.value = listResult.results
} catch (e: Exception) {
_movies.value = ArrayList()
}
}
Fetch data kita eksekusi di dalam scope coroutine untuk memastikan tidak ada thread yang terganggu ketika fetching data. Setelah itu kita update data LiveData sesuai dengan result dari apinya.
Langkah selanjutnya adalah (waduh agak panjang ya) menambahkan adapter recyclerviewnya. Kita buat file bernama MovieAdapter, tambahkan kode berikut :
class MovieAdapter : ListAdapter<Movie, MovieAdapter.ViewHolder>(DiffCalback) {
class ViewHolder(private var binding: GridViewItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(movie: Movie) {
binding.movie = movie
binding.executePendingBindings()
}
}
companion object DiffCalback : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(GridViewItemBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val movie = getItem(position)
holder.bind(movie)
}
}
Untuk layout itemnya kira-kira seperti ini hanya ada gambar dan judul filmnya :
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="movie" type="com.example.lambo.model.Movie"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/movie_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:padding="2dp"
tools:src="@tools:sample/backgrounds/scenic"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Title"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@+id/movie_image" app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Adapter ini berfungsi untuk mengatur list itemnya mulai dari klik event, layout, bahkan ketika terjadi perubahan atau update data dari listnya. Untuk update data disini saya tidak menggunkan adapter.notifyDataSetChanged() , melainkan DiffUtil callback karena DiffUtil sendiri akan mengecek perubahan data dari listnya berdasarkan id yang diberikan dan ketika terjadi perubahan data maka dia hanya akan mengupdate pada current item yang berubah, sedangkan adapter.notifyDataSetChanged() akan melakukan draw ulang seluruh data yang mungkin akan memerlukan lebih banyak waktu dalam proses update data sebuah recyclerview.
Setelah jadi kita buat sebuah file Binding Adapter yaitu adapter yang terhubung langsung dengan data binding, yang akan menyajikan data ke layoutnya. Nama filenya BindingAdapter.kt.
Pertama kita buat variable untuk base url imagenya
val BASEURL_IMAGE = https://image.tmdb.org/t/p/w185/
Kemudian buat fungsi untuk melakukan binding data ke recyclerView
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<Movie>?) {
val adapter = recyclerView.adapter as MovieAdapter
adapter.submitList(data)
}
Selanjutnya adalah fungsi binding gambar dan text pada item recyclerviewnya
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
Glide.with(imgView.context)
.load(BASEURL_IMAGE + it)
.apply(
RequestOptions()
.placeholder(R.drawable.ic_image))
.into(imgView)
}
}
@BindingAdapter("textTitle")
fun bindText(textView: TextView, text: String?) {
text?.let {
textView.text = it
}
}
Setelah itu, kita loncat dulu ke HomeFragment, pada HomeFragment kita hanya perlu melakukan binding adapter RecyclerView dan ViewModelnya saja
val binding = FragmentHomeBinding.inflate(inflater)
binding.lifecycleOwner = this
binding.viewModel = viewModel
binding.movieGrid.adapter = MovieAdapter()
return binding.root
Langkah terakhir adalah update data pada layoutnya, masuk ke fragment_home. Untuk menampilkan list data pada UI, kita tambahkan binding expression pada recyclerView
app:listData="@{viewModel.movies}"
Sedangkan untuk gambar dan judul filmnya, kita update di layout grid_view_item
app:imageUrl="@{movie.posterPath}"
app:textTitle="@{movie.title}"
Dan akan terjadi kemunculan yang seperti ini :
Sampai disini kita sudah berhasil melakukan fetch data sekaligus menampilkan data dengan Jetpack Component, Retrofit dan Kotlin Coroutine di Android, mungkin ada beberapa atau banyak kekurangan pada tulisan ini, untuk itu mohon dimaklumi karena penulis juga seorang nup yang hanya mencoba sharing.
Yang mau sourcenya bisa dilihat di github saya. Thank you and Keep Bucin Learning :)