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.
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 kategoriDELETE /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.
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
suspendfunctions 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!
Ditulis oleh Kakak programmer yang dulu juga sering lupa kasih @Body. Kalau ada pertanyaan, tulis di komentar ya!