Android Kotlin #10: Menambah, Mengubah, dan Menghapus Data (POST, PUT, DELETE)


Android Kotlin #10: Menambah, Mengubah, dan Menghapus Data (POST, PUT, DELETE)

Halo lagi, calon developer aplikasi wisata! 👋

Selama ini kita cuma bisa membaca data dari API (GET). Padahal aplikasi yang berguna pasti butuh menulis: menambah kategori baru, mengedit deskripsi, atau menghapus tempat wisata yang sudah tidak relevan. Nah, di artikel ini kita akan belajar operasi POST, PUT, dan DELETE menggunakan Retrofit, ViewModel, dan LiveData. Anggap saja kita kasih aplikasi kita kemampuan untuk ngobrol dua arah dengan server.

🤣 Kenapa POST, PUT, DELETE disebut operasi tulis? Karena mereka suka nulis di database, bukan cuma baca doang kayak GET! 😆

Yang Akan Kita Buat

  • POST: menambah kategori wisata baru via form.
  • PUT: mengedit kategori yang sudah ada (dari detail atau dialog).
  • DELETE: menghapus kategori dengan konfirmasi.
  • Memperbarui RecyclerView secara otomatis setelah operasi berhasil.

Kita akan fokus ke KategoriWisata dulu agar lebih sederhana. Nanti bisa diterapkan ke DaftarWisata dengan cara yang sama.

Langkah 0: Pastikan API Mendukung

API Wisata kita di seri sebelumnya sudah memiliki endpoint untuk CRUD kategori:

  • POST /api/kategori – tambah kategori (body JSON: {"nama":"...","deskripsi":"..."})
  • PUT /api/kategori/{id} – ubah kategori
  • DELETE /api/kategori/{id} – hapus kategori

Pastikan API berjalan dan bisa diakses dari emulator (http://10.0.2.2:7000).

Langkah 1: Update ApiService

Buka ApiService.kt dan tambahkan endpoint baru:

interface ApiService {
    // ... (GET yang sudah ada)

    @POST("api/kategori")
    suspend fun addKategori(@Body kategori: KategoriWisata): Response<KategoriWisata>

    @PUT("api/kategori/{id}")
    suspend fun updateKategori(@Path("id") id: Int, @Body kategori: KategoriWisata): Response<KategoriWisata>

    @DELETE("api/kategori/{id}")
    suspend fun deleteKategori(@Path("id") id: Int): Response<Unit>
}

Perhatikan: kita gunakan suspend agar bisa dipanggil dalam coroutine. Response<Unit> untuk DELETE yang tidak mengembalikan body.

Langkah 2: Buat ViewModel untuk Kategori (CategoryViewModel.kt)

Buat file baru CategoryViewModel.kt:

package com.example.wisataapp

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class CategoryViewModel : ViewModel() {

    private val _listKategori = MutableLiveData<List<KategoriWisata>>()
    val listKategori: LiveData<List<KategoriWisata>> get() = _listKategori

    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> get() = _isLoading

    private val _errorMessage = MutableLiveData<String?>()
    val errorMessage: LiveData<String?> get() = _errorMessage

    private val _operationSuccess = MutableLiveData<Boolean>()
    val operationSuccess: LiveData<Boolean> get() = _operationSuccess

    fun fetchKategori() {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val response = RetrofitInstance.api.getKategori().execute()
                if (response.isSuccessful) {
                    response.body()?.let {
                        _listKategori.value = it
                    }
                } else {
                    _errorMessage.value = "Gagal mengambil data: ${response.code()}"
                }
            } catch (e: Exception) {
                _errorMessage.value = "Error: ${e.message}"
            } finally {
                _isLoading.value = false
            }
        }
    }

    fun addKategori(kategori: KategoriWisata) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val response = RetrofitInstance.api.addKategori(kategori)
                if (response.isSuccessful) {
                    // Refresh data setelah berhasil tambah
                    fetchKategori()
                    _operationSuccess.value = true
                } else {
                    _errorMessage.value = "Gagal menambah: ${response.code()}"
                }
            } catch (e: Exception) {
                _errorMessage.value = "Error: ${e.message}"
            } finally {
                _isLoading.value = false
            }
        }
    }

    fun updateKategori(id: Int, kategori: KategoriWisata) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val response = RetrofitInstance.api.updateKategori(id, kategori)
                if (response.isSuccessful) {
                    fetchKategori() // refresh
                    _operationSuccess.value = true
                } else {
                    _errorMessage.value = "Gagal mengupdate: ${response.code()}"
                }
            } catch (e: Exception) {
                _errorMessage.value = "Error: ${e.message}"
            } finally {
                _isLoading.value = false
            }
        }
    }

    fun deleteKategori(id: Int) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val response = RetrofitInstance.api.deleteKategori(id)
                if (response.isSuccessful) {
                    fetchKategori() // refresh
                    _operationSuccess.value = true
                } else {
                    _errorMessage.value = "Gagal menghapus: ${response.code()}"
                }
            } catch (e: Exception) {
                _errorMessage.value = "Error: ${e.message}"
            } finally {
                _isLoading.value = false
            }
        }
    }
}

Langkah 3: Buat Activity untuk Form Tambah/Edit Kategori

Buat activity baru: AddEditCategoryActivity.kt dan layout activity_add_edit_category.xml.

Layout sederhana (activity_add_edit_category.xml):

<?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"
    android:padding="16dp">

    <EditText
        android:id="@+id/editNama"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Nama Kategori"
        android:inputType="text" />

    <EditText
        android:id="@+id/editDeskripsi"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Deskripsi"
        android:inputType="textMultiLine"
        android:layout_marginTop="8dp" />

    <Button
        android:id="@+id/btnSimpan"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Simpan"
        android:layout_marginTop="16dp" />

</LinearLayout>

Di AddEditCategoryActivity.kt:

package com.example.wisataapp

import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider

class AddEditCategoryActivity : AppCompatActivity() {

    private lateinit var editNama: EditText
    private lateinit var editDeskripsi: EditText
    private lateinit var btnSimpan: Button
    private lateinit var viewModel: CategoryViewModel
    private var kategoriId: Int? = null // null untuk tambah, tidak null untuk edit

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

        editNama = findViewById(R.id.editNama)
        editDeskripsi = findViewById(R.id.editDeskripsi)
        btnSimpan = findViewById(R.id.btnSimpan)

        viewModel = ViewModelProvider(this)[CategoryViewModel::class.java]

        // Cek apakah mode edit (ada extra "kategori")
        val kategori = intent.getParcelableExtra<KategoriWisata>("kategori")
        if (kategori != null) {
            kategoriId = kategori.id
            editNama.setText(kategori.nama)
            editDeskripsi.setText(kategori.deskripsi)
            supportActionBar?.title = "Edit Kategori"
        } else {
            supportActionBar?.title = "Tambah Kategori"
        }

        btnSimpan.setOnClickListener {
            val nama = editNama.text.toString().trim()
            val deskripsi = editDeskripsi.text.toString().trim()
            if (nama.isEmpty()) {
                Toast.makeText(this, "Nama harus diisi", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            if (kategoriId == null) {
                // Tambah baru
                val kategoriBaru = KategoriWisata(0, nama, deskripsi) // id 0 akan diabaikan server
                viewModel.addKategori(kategoriBaru)
            } else {
                // Update
                val kategoriUpdate = KategoriWisata(kategoriId!!, nama, deskripsi)
                viewModel.updateKategori(kategoriId!!, kategoriUpdate)
            }
        }

        // Observasi hasil operasi
        viewModel.operationSuccess.observe(this) { success ->
            if (success) {
                Toast.makeText(this, "Berhasil disimpan", Toast.LENGTH_SHORT).show()
                finish() // kembali ke MainActivity
            }
        }

        viewModel.errorMessage.observe(this) { error ->
            error?.let {
                Toast.makeText(this, it, Toast.LENGTH_LONG).show()
            }
        }

        viewModel.isLoading.observe(this) { loading ->
            // bisa tampilkan progress bar
        }
    }
}

Jangan lupa daftarkan activity di AndroidManifest.xml.

🖱️ Langkah 4: Modifikasi MainActivity untuk Menampilkan Kategori dan Tombol Tambah

Ubah MainActivity.kt untuk menggunakan CategoryViewModel dan menampilkan daftar kategori. Tambahkan FAB (Floating Action Button) untuk menambah kategori.

Pertama, layout activity_main.xml tambahkan RecyclerView dan FAB:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerViewKategori"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fabAdd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@android:drawable/ic_input_add"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Tambahkan dependency Material jika belum ada (untuk FAB):

implementation 'com.google.android.material:material:1.11.0'

Kemudian MainActivity.kt:

package com.example.wisataapp

import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var fabAdd: FloatingActionButton
    private lateinit var adapter: KategoriAdapter // kita perlu adapter untuk kategori

    private val categoryViewModel: CategoryViewModel by viewModels()

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

        recyclerView = findViewById(R.id.recyclerViewKategori)
        fabAdd = findViewById(R.id.fabAdd)
        recyclerView.layoutManager = LinearLayoutManager(this)

        // Observer data kategori
        categoryViewModel.listKategori.observe(this, Observer { list ->
            adapter = KategoriAdapter(list) { kategori ->
                // Item click: buka detail / edit? kita akan buat intent ke AddEditCategoryActivity dengan data
                val intent = Intent(this, AddEditCategoryActivity::class.java)
                intent.putExtra("kategori", kategori) // untuk edit
                startActivity(intent)
            }
            recyclerView.adapter = adapter
        })

        categoryViewModel.isLoading.observe(this, Observer { loading ->
            // handle loading
        })

        categoryViewModel.errorMessage.observe(this, Observer { error ->
            error?.let {
                Toast.makeText(this, it, Toast.LENGTH_LONG).show()
            }
        })

        // Tombol tambah
        fabAdd.setOnClickListener {
            val intent = Intent(this, AddEditCategoryActivity::class.java)
            startActivity(intent)
        }

        // Ambil data
        categoryViewModel.fetchKategori()
    }

    override fun onResume() {
        super.onResume()
        // Refresh data setiap kali kembali ke activity (misal setelah tambah/edit)
        categoryViewModel.fetchKategori()
    }
}

Langkah 5: Menambahkan Fitur Hapus (Swipe to Delete atau Tombol)

Ada beberapa cara: menambahkan tombol hapus di item, atau swipe to delete. Kita akan gunakan swipe to delete karena lebih modern dan hemat tempat.

Tambahkan ItemTouchHelper di MainActivity setelah adapter diset:

// Di MainActivity, setelah adapter diset (misal di observer)
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        return false
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        val position = viewHolder.adapterPosition
        val kategori = adapter.getKategoriAt(position) // kita perlu fungsi ini di adapter
        // Tampilkan dialog konfirmasi
        AlertDialog.Builder(this@MainActivity)
            .setTitle("Hapus Kategori")
            .setMessage("Yakin ingin menghapus ${kategori.nama}?")
            .setPositiveButton("Hapus") { _, _ ->
                categoryViewModel.deleteKategori(kategori.id)
            }
            .setNegativeButton("Batal") { _, _ ->
                adapter.notifyItemChanged(position) // batalkan swipe
            }
            .show()
    }
})
itemTouchHelper.attachToRecyclerView(recyclerView)

Untuk itu, kita perlu menambahkan fungsi getKategoriAt(position) di KategoriAdapter:

fun getKategoriAt(position: Int): KategoriWisata = listKategori[position]

Jangan lupa import AlertDialog dan ItemTouchHelper.

Setelah delete berhasil, observer akan memuat ulang data otomatis (karena kita panggil fetchKategori() di ViewModel setelah delete).

Langkah 6: Jalankan dan Uji

  • Jalankan API.
  • Jalankan aplikasi Android.
  • Klik FAB + untuk menambah kategori baru. Isi form dan simpan. Harusnya berhasil dan kembali ke MainActivity dengan data baru.
  • Klik salah satu item untuk masuk ke mode edit. Ubah nama/deskripsi dan simpan. Perubahan akan tampil.
  • Swipe kiri/kanan pada item, konfirmasi hapus. Item akan terhapus.
💡 Jika operasi gagal, pastikan API berjalan dan endpoint benar. Cek Logcat untuk melihat error detail. Bisa juga tambahkan logging interceptor di Retrofit.

Selamat! Aplikasi Kamu Sekarang Bisa CRUD

Dengan ini, aplikasi wisata kamu sudah memiliki kemampuan lengkap: membaca, menambah, mengubah, dan menghapus data. Kamu sudah mengimplementasikan arsitektur modern dengan ViewModel, LiveData, dan Retrofit. Mantap!

Rangkuman

  • POST: mengirim data baru ke server, biasanya menggunakan @Body.
  • PUT: mengirim data lengkap untuk update (bisa juga PATCH untuk sebagian).
  • DELETE: menghapus data berdasarkan id.
  • Gunakan suspend functions di interface Retrofit agar bisa dipanggil dalam coroutine.
  • Setelah operasi tulis, panggil fetchKategori() untuk memperbarui LiveData dan RecyclerView.
  • ItemTouchHelper memudahkan implementasi swipe to delete.

Selanjutnya?

Di artikel berikutnya (Android Kotlin #11: Autentikasi – Login dan Menyimpan Token JWT), kita akan menambahkan sistem login. Karena API kita sudah dilindungi JWT, kita perlu mengirim token di header setiap request. Kita juga akan menyimpan token dengan DataStore. Sampai jumpa!

😎 Kalau CRUD-mu error, jangan panik. Cek lagi endpoint, cek lagi data yang dikirim. Ingat, POST itu kayak ngirim surat: alamatnya harus benar, isinya harus sesuai!

Ditulis oleh Kakak programmer yang dulu juga sering lupa kasih @Body. Kalau ada pertanyaan, tulis di komentar ya!

Lebih baru Lebih lama

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