Android Kotlin #7: Menampilkan Gambar dengan Glide/Coil


Android Kotlin #7: Menampilkan Gambar dengan Glide/Coil

Halo lagi, calon developer aplikasi wisata! 

Di artikel sebelumnya kita sudah berhasil mengambil data kategori dari API. Sekarang kita akan naik level: menampilkan gambar untuk setiap tempat wisata. Di API Wisata kita, setiap tempat wisata punya properti imagePath yang berisi URL gambar (misal /uploads/gambar.jpg). Nah, tugas kita sekarang adalah menampilkan gambar-gambar itu di aplikasi Android.

🤣 Kenapa gambar di Android butuh library khusus? Karena kalau manual, kita harus download, cache, decode, dan handle error sendiri—mirip masak rame-rame pake panci yang gede banget! 😆

Pilihan Library: Glide vs Coil

Ada dua library populer untuk menampilkan gambar dari internet:

  • Glide: library mature (dewasa) yang sudah ada sejak lama, banyak digunakan, dan stabil [citation:2].
  • Coil: library modern yang ditulis dengan Kotlin dan memanfaatkan Coroutine, lebih ringan [citation:1][citation:3].

Untuk project kita, kita akan pakai Glide karena lebih umum dan mudah dicari referensinya. Tapi nanti akan saya kasih catatan juga kalau mau pakai Coil.

Langkah 1: Tambahkan Dependency Glide

Buka file build.gradle (Module: app). Di bagian dependencies, tambahkan baris berikut [citation:2][citation:5]:

implementation 'com.github.bumptech.glide:glide:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'

Klik Sync Now di pojok kanan atas.

💡 Versi bisa disesuaikan dengan yang terbaru. Cek di GitHub Glide.

Langkah 2: Update Model DaftarWisata

Kita perlu menambahkan field imagePath di model DaftarWisata (kalau belum ada). Buat file baru atau buka DaftarWisata.kt:

package com.example.wisataapp

data class DaftarWisata(
    val id: Int,
    val nama: String,
    val deskripsi: String?,
    val lokasi: String?,
    val hargaTiket: Double?,
    val kategoriId: Int,
    val imagePath: String?  // 👈 tambahkan ini
)

Langkah 3: Buat Layout untuk Item Wisata (item_wisata.xml)

Buat file layout baru: res/layout/item_wisata.xml. Isinya:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="8dp">

        <!-- ImageView untuk foto -->
        <ImageView
            android:id="@+id/imageWisata"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:scaleType="centerCrop"
            android:src="@drawable/placeholder_image" />

        <!-- Detail teks di sebelah kanan -->
        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginStart="8dp"
            android:orientation="vertical">

            <TextView
                android:id="@+id/textNama"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="18sp"
                android:textStyle="bold"
                android:textColor="@android:color/black" />

            <TextView
                android:id="@+id/textLokasi"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="14sp"
                android:layout_marginTop="4dp"
                android:textColor="@android:color/darker_gray" />

            <TextView
                android:id="@+id/textHarga"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="14sp"
                android:layout_marginTop="4dp"
                android:textColor="@android:color/holo_green_dark" />
        </LinearLayout>
    </LinearLayout>
</androidx.cardview.widget.CardView>

Catatan: @drawable/placeholder_image adalah gambar sementara (placeholder) sebelum gambar asli dimuat. Kamu bisa buat file PNG kecil atau pakai vector asset dari Android Studio.

Cara buat placeholder: klik kanan res/drawable → New → Vector Asset, pilih ikon (misal ic_image). Atau download gambar polos dari internet.

Langkah 4: Buat Adapter untuk DaftarWisata (WisataAdapter.kt)

Buat class baru WisataAdapter.kt:

package com.example.wisataapp

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners

class WisataAdapter(
    private val listWisata: List<DaftarWisata>,
    private val onItemClick: (DaftarWisata) -> Unit
) : RecyclerView.Adapter<WisataAdapter.WisataViewHolder>() {

    class WisataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val imageWisata: ImageView = itemView.findViewById(R.id.imageWisata)
        val textNama: TextView = itemView.findViewById(R.id.textNama)
        val textLokasi: TextView = itemView.findViewById(R.id.textLokasi)
        val textHarga: TextView = itemView.findViewById(R.id.textHarga)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WisataViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_wisata, parent, false)
        return WisataViewHolder(view)
    }

    override fun onBindViewHolder(holder: WisataViewHolder, position: Int) {
        val wisata = listWisata[position]

        holder.textNama.text = wisata.nama
        holder.textLokasi.text = wisata.lokasi ?: "Lokasi tidak diketahui"
        holder.textHarga.text = if (wisata.hargaTiket != null) 
            "Rp ${wisata.hargaTiket}" else "Gratis"

        // Load gambar dengan Glide
        Glide.with(holder.itemView.context)
            .load(wisata.imagePath) // URL gambar
            .placeholder(R.drawable.placeholder_image) // tampil saat loading
            .error(R.drawable.error_image) // tampil jika gagal
            .transform(CenterCrop(), RoundedCorners(8)) // biar rapi
            .into(holder.imageWisata)

        // Klik listener
        holder.itemView.setOnClickListener {
            onItemClick(wisata)
        }
    }

    override fun getItemCount(): Int = listWisata.size
}

Penjelasan method Glide [citation:5][citation:9]:

  • with(context): menyediakan context.
  • load(url): URL gambar (bisa String atau URI).
  • placeholder(): gambar sementara sebelum gambar asli dimuat [citation:2].
  • error(): gambar alternatif kalau gagal load [citation:2].
  • transform(): memanipulasi gambar (misal crop, rounded corners).
  • into(imageView): menaruh hasil ke ImageView.

Langkah 5: Update MainActivity untuk Mengambil Daftar Wisata

Sekarang kita ubah MainActivity.kt untuk mengambil data wisata (bukan kategori). Kita asumsikan API punya endpoint /api/wisata yang mengembalikan daftar wisata lengkap dengan gambar.

Pertama, update ApiService.kt dengan endpoint baru:

interface ApiService {
    @GET("api/kategori")
    fun getKategori(): Call<List<KategoriWisata>>

    @GET("api/wisata")
    fun getWisata(): Call<List<DaftarWisata>>
}

Kemudian di MainActivity.kt, ganti dengan kode berikut:

package com.example.wisataapp

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: WisataAdapter

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

        recyclerView = findViewById(R.id.recyclerViewKategori) // kita pakai RecyclerView yang sama
        recyclerView.layoutManager = LinearLayoutManager(this)

        fetchWisata()
    }

    private fun fetchWisata() {
        lifecycleScope.launch {
            try {
                val response = RetrofitInstance.api.getWisata().execute()
                if (response.isSuccessful) {
                    val listWisata = response.body()
                    if (listWisata != null) {
                        adapter = WisataAdapter(listWisata) { wisata ->
                            Toast.makeText(this@MainActivity, "Memilih: ${wisata.nama}", Toast.LENGTH_SHORT).show()
                            // nanti bisa intent ke detail activity
                        }
                        recyclerView.adapter = adapter
                    }
                } else {
                    Toast.makeText(this@MainActivity, "Gagal mengambil data: ${response.code()}", Toast.LENGTH_SHORT).show()
                }
            } catch (e: Exception) {
                Toast.makeText(this@MainActivity, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

Langkah 6: Jalankan Aplikasi

Pastikan API Wisata berjalan (di Visual Studio) dan endpoint /api/wisata mengembalikan data dengan imagePath. Jalankan aplikasi Android.

Jika berhasil, RecyclerView akan menampilkan daftar wisata dengan gambar, nama, lokasi, dan harga. Gambar akan muncul setelah beberapa saat (tergantung koneksi).

💡 Jika gambar tidak muncul, cek beberapa hal:
  • Pastikan URL gambar lengkap (misal http://10.0.2.2:7000/uploads/namafile.jpg).
  • Pastikan file gambar benar-benar ada di folder wwwroot/uploads di server.
  • Di Logcat, filter dengan "Glide" untuk melihat error detail.
  • Coba akses URL gambar langsung dari browser emulator (buka browser di emulator, ketik URL-nya).

Bonus: Membuat Placeholder dan Error Image

Placeholder dan error image bisa dibuat dengan vector asset:

  1. Klik kanan res/drawable → New → Vector Asset.
  2. Pilih ikon image (untuk placeholder) dan broken image (untuk error).
  3. Beri nama ic_placeholder.xml dan ic_broken_image.xml.

Lalu di kode Glide, gunakan:

.placeholder(R.drawable.ic_placeholder)
.error(R.drawable.ic_broken_image)

Alternatif dengan Coil (untuk yang penasaran)

Coil adalah library modern yang ditulis dengan Kotlin. Cara pakainya mirip, tapi dependency-nya berbeda [citation:1][citation:3]:

// build.gradle
implementation("io.coil-kt:coil:2.6.0")

// di adapter (dalam onBindViewHolder)
holder.imageWisata.load(wisata.imagePath) {
    placeholder(R.drawable.placeholder_image)
    error(R.drawable.error_image)
    transformations(CenterCrop(), RoundedCorners(8f))
}

Coil punya extension function load yang bisa langsung dipanggil di ImageView. Lebih ringkas dan Kotlin-idiomatic.

Selamat! Sekarang Aplikasi Bisa Menampilkan Gambar

Dengan Glide (atau Coil), aplikasi Android-mu sekarang bisa menampilkan foto-foto tempat wisata dari server. Ini bikin aplikasi jauh lebih menarik dan informatif.

Rangkuman

  • Glide/Coil: library untuk loading gambar dari internet dengan fitur caching, placeholder, dan error handling.
  • Placeholder: gambar sementara saat loading.
  • Error image: gambar alternatif jika gagal load.
  • Transformations: mengubah tampilan gambar (crop, rounded corners, dll).

Selanjutnya?

Di artikel berikutnya (Android Kotlin #8: Detail Screen – Berpindah Halaman dan Mengirim Data), kita akan membuat halaman detail tempat wisata. Saat item diklik, akan membuka activity/fragment baru yang menampilkan informasi lengkap dan gambar besar. Sampai jumpa!

😎 Kalau gambar masih error, jangan putus asa. Coba cek lagi path-nya, mungkin ada typo atau lupa kasih base URL. Ingat, gambar itu kayak gebetan—kadang susah muncul, tapi kalau sudah muncul, bikin senang!

Ditulis oleh Kakak programmer yang dulu juga pusing ngurusin gambar. Kalau ada pertanyaan, tulis di komentar ya!

Lebih baru Lebih lama

نموذج الاتصال