Android RecyclerView’da Kaydırarak Silme ve Geri Alma (Swipe To Delete And Undo) Uygulaması
Giriş
Android uygulamalarında liste tabanlı verileri verimli bir şekilde görüntülemek ve yönetmek için RecyclerView vazgeçilmez bir bileşendir. Kullanıcıların büyük veri setleri arasında akıcı bir şekilde gezinmelerini sağlarken, aynı zamanda listelenen öğelerle etkileşime girmelerine olanak tanır. Kullanıcı deneyimini (UX) zenginleştiren ve uygulamanın sezgiselliğini artıran yaygın etkileşimlerden biri, bir liste öğesini kaydırarak (swipe) silme ve bu işlemi yanlışlıkla yapıldığında geri alma (undo) yeteneğidir. Bu özellik, Google’ın Materyal Tasarım yönergeleriyle uyumlu olup, kullanıcılara modern ve etkileşimli bir arayüz sunar.
Bu makalede, Android RecyclerView içerisinde kaydırarak silme ve geri alma işlevini adım adım nasıl uygulayacağımızı detaylı bir şekilde inceleyeceğiz. ItemTouchHelper sınıfının gücünü kullanarak kaydırma hareketlerini nasıl yakalayacağımızı, silme işlemini nasıl gerçekleştireceğimizi ve Snackbar bileşenini kullanarak kullanıcılara geri alma seçeneğini nasıl sunacağımızı öğreneceğiz. Ayrıca, kaydırma sırasında öğenin arkasında özel bir arka plan veya simge gösterme gibi görsel özelleştirmeleri de ele alacağız. Bu kılavuz, hem yeni başlayanlar hem de deneyimli Android geliştiricileri için kapsamlı bir kaynak olmayı hedeflemektedir.
Ön Koşullar ve Temel Kavramlar
Bu makaleden en iyi şekilde faydalanmak için temel düzeyde Android geliştirme bilgisine, Kotlin veya Java programlama diline hakimiyete ve RecyclerView‘ın temel çalışma prensiplerine aşina olmanız beklenmektedir.
RecyclerView Nedir?
RecyclerView, büyük veri setlerini verimli bir şekilde görüntülemek için tasarlanmış güçlü ve esnek bir widget’tır. ListView‘a göre daha gelişmiş bir mimariye sahiptir ve öğelerin yeniden kullanımını (view recycling) optimize ederek bellek ve performans açısından önemli avantajlar sunar. Bir RecyclerView kullanmak için bir Adapter (veri setini görünüm öğelerine bağlar) ve bir LayoutManager (öğelerin ekranda nasıl düzenleneceğini belirler) gereklidir.
ItemTouchHelper Nedir?
ItemTouchHelper, RecyclerView öğelerine sürükle-bırak (drag-and-drop) ve kaydırarak silme (swipe-to-dismiss) işlevselliği eklemeyi kolaylaştıran bir yardımcı sınıftır. Bu sınıf, dokunma olaylarını dinler ve bu hareketleri RecyclerView‘daki belirli öğelerle ilişkilendirerek özel davranışlar tetiklememize olanak tanır. ItemTouchHelper, dokunma olaylarını kendisi işler ve bize yalnızca hangi hareketlerin destekleneceğini ve bu hareketler gerçekleştiğinde ne yapılacağını bildiren bir Callback arayüzü sağlar.
ItemTouchHelper.Callback Nedir?
ItemTouchHelper.Callback, ItemTouchHelper ile RecyclerView arasındaki köprü görevi gören soyut bir sınıftır. Bu sınıfı genişleterek, kaydırma ve sürükleme hareketlerinin nasıl ele alınacağını tanımlarız. Başlıca metotları şunlardır:
* getMovementFlags(): Hangi hareketlerin (kaydırma yönleri, sürükleme yönleri) destekleneceğini tanımlar.
* onMove(): Bir öğe sürüklendiğinde çağrılır.
* onSwiped(): Bir öğe tamamen kaydırıldığında çağrılır.
* onChildDraw(): Kaydırma veya sürükleme sırasında öğenin görünümünü özelleştirmek için kullanılır.
Snackbar Nedir ve Neden Kullanılır?
Snackbar, ekranın alt kısmında kısa süreli mesajlar göstermek için kullanılan hafif bir geri bildirim bileşenidir. Genellikle bir eylemin sonucunu bildirmek veya kullanıcılara bir eylemi geri alma (undo) seçeneği sunmak için kullanılır. Kaydırarak silme işleminde, öğe silindikten hemen sonra bir Snackbar göstererek “Öğe silindi” mesajını ve “Geri Al” (UNDO) düğmesini sunmak, kullanıcı deneyimi açısından oldukça önemlidir. Snackbar belirli bir süre sonra otomatik olarak kaybolur, ancak kullanıcı “Geri Al” düğmesine basarsa, silme işlemi iptal edilebilir.
Proje Yapılandırması ve Temel Bileşenler
Öncelikle, projemizin temel yapısını oluşturalım.
Bağımlılıkların Eklenmesi
build.gradle (Module: app) dosyanıza RecyclerView ve Materyal Tasarım kütüphanelerini ekleyin:
dependencies {
// ... diğer bağımlılıklar
implementation 'androidx.recyclerview:recyclerview:1.2.1' // Veya en güncel sürüm
implementation 'com.google.android.material:material:1.5.0' // Veya en güncel sürüm
}
Veri Modeli Oluşturma
Listemizde görüntülenecek öğeler için basit bir veri sınıfı tanımlayalım.
// data/MyItem.kt
package com.example.swipetodeleteundo.data
data class MyItem(val id: Int, val text: String)
Liste Öğesi Düzeni (item_row.xml)
RecyclerView‘daki her bir öğenin nasıl görüneceğini tanımlayan bir layout dosyası oluşturalım (res/layout/item_row.xml). Basit bir TextView yeterli olacaktır.
Ana Aktivite Düzeni (activity_main.xml)
Ana aktivitemizin layout dosyasına bir RecyclerView ekleyelim.
CoordinatorLayout kullanmamızın nedeni, Snackbar‘ın doğru bir şekilde görünmesini ve RecyclerView ile etkileşimini sağlamaktır.
RecyclerView Adaptörü Oluşturma
Veri setimizi RecyclerView‘a bağlayacak adaptörü oluşturalım. Bu adaptör, veri öğelerini alacak ve bunları item_row.xml düzenine bağlayacaktır.
// adapter/MyAdapter.kt
package com.example.swipetodeleteundo.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.swipetodeleteundo.R
import com.example.swipetodeleteundo.data.MyItem
class MyAdapter(private val items: MutableList) :
RecyclerView.Adapter() {
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val itemTextView: TextView = itemView.findViewById(R.id.itemTextView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_row, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.itemTextView.text = items[position].text
}
override fun getItemCount(): Int = items.size
// Silme ve geri alma işlemleri için yardımcı metotlar
fun removeItem(position: Int) {
items.removeAt(position)
notifyItemRemoved(position)
}
fun restoreItem(item: MyItem, position: Int) {
items.add(position, item)
notifyItemInserted(position)
}
fun getData(): MutableList = items
}
removeItem ve restoreItem metotları, silme ve geri alma işlemlerini adaptör üzerinden yönetmemizi sağlar. notifyItemRemoved ve notifyItemInserted metotları, RecyclerView‘ın yalnızca etkilenen öğeyi yeniden çizmesini söyleyerek performans optimizasyonu sağlar.
ItemTouchHelper.Callback Uygulaması
Şimdi ItemTouchHelper.Callback sınıfını genişleterek kaydırma davranışını tanımlayalım.
// util/SwipeToDeleteCallback.kt
package com.example.swipetodeleteundo.util
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.ColorDrawable
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.example.swipetodeleteundo.R
import com.google.android.material.snackbar.Snackbar
class SwipeToDeleteCallback(private val adapter: MyAdapter, private val context: Context) :
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.START or ItemTouchHelper.END) {
private val deleteIcon = ContextCompat.getDrawable(context, R.drawable.ic_delete) // ic_delete.xml dosyasını oluşturacağız
private val intrinsicWidth = deleteIcon?.intrinsicWidth ?: 0
private val intrinsicHeight = deleteIcon?.intrinsicHeight ?: 0
private val background = ColorDrawable()
private val backgroundColor = Color.parseColor("#f44336") // Kırmızı renk
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
// Sadece kaydırma hareketlerini destekle, sürüklemeyi değil.
// START (sağa kaydırma) ve END (sola kaydırma) yönlerini etkinleştir.
return makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
// Sürükleme hareketlerini desteklemediğimiz için burada false döneriz.
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
val deletedItem = adapter.getData()[position] // Silinecek öğeyi yedekle
adapter.removeItem(position) // Adaptörden öğeyi kaldır
// Snackbar göstererek geri alma seçeneği sun
Snackbar.make(
viewHolder.itemView,
"${deletedItem.text} silindi",
Snackbar.LENGTH_LONG
).apply {
setAction("GERİ AL") {
adapter.restoreItem(deletedItem, position) // Geri al butonuna basılırsa öğeyi geri ekle
this.dismiss() // Snackbar'ı kapat
}
setBackgroundTint(ContextCompat.getColor(context, R.color.snackbar_background)) // Renk tanımlaması yapılacak
setActionTextColor(ContextCompat.getColor(context, R.color.snackbar_action_text)) // Renk tanımlaması yapılacak
show()
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val itemView = viewHolder.itemView
val itemHeight = itemView.bottom - itemView.top
val isCanceled = dX == 0f && !isCurrentlyActive
if (isCanceled) {
clearCanvas(c, itemView.right + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat())
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
return
}
// Arka planı çiz
background.color = backgroundColor
if (dX > 0) { // Sağa kaydırma (öğe sağa doğru gidiyor, arka plan solda beliriyor)
background.setBounds(itemView.left, itemView.top, itemView.left + dX.toInt(), itemView.bottom)
} else { // Sola kaydırma (öğe sola doğru gidiyor, arka plan sağda beliriyor)
background.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
}
background.draw(c)
// Silme simgesini çiz
val deleteIconTop = itemView.top + (itemHeight - intrinsicHeight) / 2
val deleteIconBottom = deleteIconTop + intrinsicHeight
val deleteIconMargin = (itemHeight - intrinsicHeight) / 2
if (dX > 0) { // Sağa kaydırma
val deleteIconLeft = itemView.left + deleteIconMargin
val deleteIconRight = itemView.left + deleteIconMargin + intrinsicWidth
deleteIcon?.setBounds(deleteIconLeft, deleteIconTop, deleteIconRight, deleteIconBottom)
} else { // Sola kaydırma
val deleteIconLeft = itemView.right - deleteIconMargin - intrinsicWidth
val deleteIconRight = itemView.right - deleteIconMargin
deleteIcon?.setBounds(deleteIconLeft, deleteIconTop, deleteIconRight, deleteIconBottom)
}
deleteIcon?.draw(c)
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
private fun clearCanvas(c: Canvas, left: Float, top: Float, right: Float, bottom: Float) {
c.drawRect(left, top, right, bottom, Paint().apply { color = Color.TRANSPARENT })
}
}
Renk ve İkon Tanımlamaları:
Yukarıdaki kodda kullanılan ic_delete ikonunu ve snackbar_background, snackbar_action_text renklerini tanımlamanız gerekmektedir.
1. ic_delete.xml (Vector Asset): res/drawable klasörüne sağ tıklayın -> New -> Vector Asset. “Delete” ikonunu arayın ve projenize ekleyin.
2. colors.xml: res/values/colors.xml dosyanıza aşağıdaki renkleri ekleyin:
#323232
#FFC107
SwipeToDeleteCallback Açıklamaları:
* getMovementFlags(): Bu metot, hangi hareketlerin etkinleştirileceğini belirtir. ItemTouchHelper.LEFT ve ItemTouchHelper.RIGHT kullanarak hem sola hem de sağa kaydırmayı etkinleştirdik. 0 değeri sürükleme hareketlerini devre dışı bırakır.
* onMove(): Sürükleme hareketlerini desteklemediğimiz için bu metotta her zaman false döndürürüz.
* onSwiped(): Bir öğe tamamen kaydırıldığında bu metot çağrılır.
* adapterPosition ile kaydırılan öğenin pozisyonunu alırız.
* adapter.getData()[position] ile silinecek öğeyi geçici olarak deletedItem değişkenine kaydederiz. Bu, geri alma işlemi için önemlidir.
* adapter.removeItem(position) çağrısıyla adaptörden öğeyi kaldırırız. Adaptör, notifyItemRemoved(position) ile RecyclerView‘ı günceller.
* Snackbar.make() ile kullanıcıya geri bildirim ve “Geri Al” seçeneği sunarız. setAction() metodu ile “Geri Al” düğmesine basıldığında çalışacak kodu tanımlarız. Bu kod, adapter.restoreItem() çağırarak öğeyi orijinal pozisyonuna geri ekler.
* onChildDraw(): Bu metot, kaydırma sırasında öğenin arkasındaki görseli (arka plan rengi ve silme ikonu) çizmek için kullanılır.
* dX değeri, öğenin yatayda ne kadar kaydırıldığını gösterir. Pozitif değer sağa, negatif değer sola kaydırmayı ifade eder.
* Arka plan rengi (backgroundColor) ve silme ikonu (deleteIcon) dX değerine göre öğenin solunda veya sağında konumlandırılarak çizilir.
* super.onChildDraw() çağrısı, öğenin kendisinin kaydırılma animasyonunu doğru bir şekilde yönetir.
* clearCanvas(): Öğenin kaydırma animasyonu bittiğinde, arka planı temizlemek için kullanılır. Bu, özellikle onChildDraw içinde özel çizim yapıldığında önemlidir.
ItemTouchHelper’ı RecyclerView’a Bağlama
Son olarak, MainActivity içerisinde RecyclerView‘ı, adaptörü ve ItemTouchHelper‘ı bir araya getirelim.
// MainActivity.kt
package com.example.swipetodeleteundo
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.swipetodeleteundo.adapter.MyAdapter
import com.example.swipetodeleteundo.data.MyItem
import com.example.swipetodeleteundo.util.SwipeToDeleteCallback
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var myAdapter: MyAdapter
private val itemList = mutableListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById(R.id.recyclerView)
setupRecyclerView()
populateItemList()
}
private fun setupRecyclerView() {
myAdapter = MyAdapter(itemList)
recyclerView.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = myAdapter
}
// SwipeToDeleteCallback'i oluştur ve ItemTouchHelper'a bağla
val swipeHandler = SwipeToDeleteCallback(myAdapter, this)
val itemTouchHelper = ItemTouchHelper(swipeHandler)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
private fun populateItemList() {
for (i in 1..20) {
itemList.add(MyItem(i, "Öğe $i"))
}
myAdapter.notifyDataSetChanged()
}
}
MainActivity Açıklamaları:
* setupRecyclerView() metodu içerisinde MyAdapter‘ı oluşturup RecyclerView‘a atarız.
* En önemli kısım: SwipeToDeleteCallback sınıfımızın bir örneğini oluştururuz. Bu örnek, adaptörümüzü ve Context‘i parametre olarak alır.
* Ardından, bu SwipeToDeleteCallback örneğini kullanarak bir ItemTouchHelper nesnesi oluştururuz.
* Son olarak, itemTouchHelper.attachToRecyclerView(recyclerView) çağrısı ile ItemTouchHelper‘ı RecyclerView‘a bağlarız. Bu sayede ItemTouchHelper, RecyclerView üzerindeki dokunma olaylarını dinlemeye ve SwipeToDeleteCallback içerisindeki metotları tetiklemeye başlar.
* populateItemList() metodu, test amaçlı olarak listeye örnek öğeler ekler.
Bu adımları tamamladığınızda, uygulamanızı çalıştırdığınızda RecyclerView‘daki öğeleri sola veya sağa kaydırarak silebilir ve çıkan Snackbar aracılığıyla bu işlemi geri alabilirsiniz.
Gelişmiş Konular ve En İyi Uygulamalar
Kalıcı Veri Yönetimi (Veritabanı/API)
Şu anki uygulamamızda silme işlemi yalnızca uygulama belleğindeki MutableList üzerinde gerçekleşmektedir. Gerçek dünya uygulamalarında, silinen öğelerin bir veritabanından (Room, SQLite) veya bir uzak API’den (RESTful servisler) kalıcı olarak kaldırılması gerekir.
* Silme Onayı: Kullanıcı bir öğeyi sildiğinde, Snackbar gösterilirken arka planda bir zamanlayıcı başlatılabilir. Snackbar kaybolduğunda veya “Geri Al” butonuna basılmadığında, zamanlayıcı biter ve öğe veritabanından/API’den kalıcı olarak silinir.
* Hata Yönetimi: Kalıcı silme işlemi sırasında bir ağ hatası veya veritabanı hatası oluşursa, kullanıcıya bilgi verilmeli ve gerekirse öğe listeye geri eklenmelidir.
Performans İyileştirmeleri (DiffUtil)
Büyük veri setleriyle çalışırken, notifyDataSetChanged() yerine DiffUtil kullanmak performans açısından çok daha verimlidir. DiffUtil, iki liste arasındaki farkları hesaplar ve RecyclerView‘a yalnızca değişen, eklenen veya silinen öğeleri güncellemesini söyler. Bu, UI güncellemelerinin daha akıcı olmasını sağlar. DiffUtil‘i kullanmak için ListAdapter veya RecyclerView.Adapter içinde DiffUtil.Callback‘i implement etmeniz gerekir.
Kullanıcı Deneyimi İpuçları
* Görsel Geri Bildirim: Kaydırma sırasında arka plan rengi ve silme ikonu gibi görsel ipuçları, kullanıcının ne olduğunu anlamasına yardımcı olur.
* Haptik Geri Bildirim: Kaydırma işlemi tamamlandığında hafif bir titreşim (haptic feedback) eklemek, kullanıcıya fiziksel bir geri bildirim sağlayarak deneyimi zenginleştirebilir.
* Tutarlı Davranış: Uygulamanızdaki tüm kaydırarak silme işlemleri tutarlı bir şekilde çalışmalı ve benzer görsel geri bildirimler sunmalıdır.
* Engelleme: Öğe kaydırılırken, kullanıcı başka bir öğeyi kaydırmamalı veya başka bir işlem yapmamalıdır. Genellikle ItemTouchHelper bunu otomatik olarak yönetir.
Çoklu Kaydırma ve Kenar Durumlar
* Aynı Anda Birden Fazla Silme: Snackbar yalnızca tek bir öğenin geri alınmasına izin verir. Birden fazla öğeyi aynı anda kaydırarak silme senaryolarında, her silme işlemi için ayrı bir Snackbar göstermek veya daha karmaşık bir geri alma mekanizması tasarlamak gerekebilir.
* Boş Liste: Liste boşaldığında kullanıcıya uygun bir “boş durum” (empty state) mesajı veya görseli gösterilmelidir.
* Ekran Döndürme: Uygulama ekran döndüğünde (configuration change), deletedItem ve position gibi geçici verilerin korunması önemlidir. Bu, ViewModel veya onSaveInstanceState() gibi mekanizmalarla sağlanabilir.
Özel Kaydırma Eşikleri ve Animasyonlar
ItemTouchHelper.Callback sınıfı, kaydırma eşiklerini (getSwipeThreshold(), getSwipeEscapeMaxVelocity(), getSwipeVelocityThreshold()) ve animasyon sürelerini (getAnimationDuration()) özelleştirmenize olanak tanır. Bu metotları override ederek, kaydırma hareketinin ne kadar hızlı veya ne kadar uzağa gitmesi gerektiğini belirleyebilir, böylece uygulamanızın kaydırma davranışını daha hassas bir şekilde kontrol edebilirsiniz.
Sonuç
Bu makalede, Android RecyclerView‘da kaydırarak silme ve geri alma işlevini adım adım nasıl uygulayacağımızı öğrendik. ItemTouchHelper‘ın gücünü kullanarak kaydırma hareketlerini yakaladık, Snackbar ile geri alma seçeneği sunduk ve onChildDraw metodu ile kaydırma sırasında görsel geri bildirimi özelleştirdik. Bu özellik, modern Android uygulamalarında kullanıcı deneyimini önemli ölçüde artıran ve uygulamanıza profesyonel bir dokunuş katan güçlü bir araçtır.
Uygulamanızı geliştirirken, bu temel yapıyı kendi özel ihtiyaçlarınıza göre uyarlayabilir, veritabanı entegrasyonu, gelişmiş hata yönetimi ve daha karmaşık kullanıcı arayüzü etkileşimleri gibi gelişmiş konuları entegre edebilirsiniz. Unutmayın ki, iyi bir kullanıcı deneyimi, sadece işlevselliği sağlamakla kalmaz, aynı zamanda kullanıcıların uygulamanızla sezgisel ve keyifli bir şekilde etkileşime girmesini de sağlar. Bu kılavuz, bu tür etkileşimleri başarıyla uygulamanız için sağlam bir temel sunmaktadır.
