Giới thiệu
Ví dụ hoàn chỉnh minh họa cách kết nối RecyclerView với cơ sở dữ liệu SQLite trên Android sử dụng Kotlin. Bao gồm: CRUD (Thêm, Xem, Sửa, Xóa) danh bạ, Custom Adapter, Dialog nhập liệu, và SQLiteOpenHelper.
Kiến trúc tổng quan
- Model:
Contacts(data class). - Database:
SqliteDatabase(extends SQLiteOpenHelper). - ViewHolder:
ContactViewHolder(bind view). - Adapter:
ContactAdapter(RecyclerView.Adapter + Filterable). - UI:
MainActivity(RecyclerView, Button ADD, Dialog).
Các file code chi tiết
1. Model: Contacts.kt
data class Contacts(
var id: Int = 0,
var name: String = "",
var phno: String = ""
)
2. Database: SqliteDatabase.kt
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class SqliteDatabase(context: Context?) : SQLiteOpenHelper(
context, DATABASE_NAME, null, DATABASE_VERSION
) {
companion object {
private const val DATABASE_VERSION = 1
private const val DATABASE_NAME = "ContactsDB"
private const val TABLE_CONTACTS = "contacts"
private const val COLUMN_ID = "_id"
private const val COLUMN_NAME = "name"
private const val COLUMN_PHONE = "phone"
}
override fun onCreate(db: SQLiteDatabase) {
val sql = "CREATE TABLE $TABLE_CONTACTS (" +
"$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, " +
"$COLUMN_NAME TEXT, " +
"$COLUMN_PHONE TEXT)"
db.execSQL(sql)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS $TABLE_CONTACTS")
onCreate(db)
}
// CREATE
fun addContact(contact: Contacts): Long {
val db = writableDatabase
val values = ContentValues().apply {
put(COLUMN_NAME, contact.name)
put(COLUMN_PHONE, contact.phno)
}
return db.insert(TABLE_CONTACTS, null, values)
}
// READ ALL
fun getAllContacts(): List {
val list = mutableListOf()
val db = readableDatabase
val cursor = db.query(TABLE_CONTACTS, null, null, null, null, null, "$COLUMN_NAME ASC")
with(cursor) {
while (moveToNext()) {
list.add(Contacts(
id = getInt(getColumnIndexOrThrow(COLUMN_ID)),
name = getString(getColumnIndexOrThrow(COLUMN_NAME)),
phno = getString(getColumnIndexOrThrow(COLUMN_PHONE))
))
}
close()
}
return list
}
// UPDATE
fun updateContact(contact: Contacts): Int {
val db = writableDatabase
val values = ContentValues().apply {
put(COLUMN_NAME, contact.name)
put(COLUMN_PHONE, contact.phno)
}
return db.update(TABLE_CONTACTS, values, "$COLUMN_ID = ?", arrayOf(contact.id.toString()))
}
// DELETE
fun deleteContact(id: Int): Int {
val db = writableDatabase
return db.delete(TABLE_CONTACTS, "$COLUMN_ID = ?", arrayOf(id.toString()))
}
}
3. ViewHolder: ContactViewHolder.kt
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class ContactViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvName: TextView = itemView.findViewById(R.id.contactName)
val tvPhone: TextView = itemView.findViewById(R.id.phoneNum)
val ivEdit: ImageView = itemView.findViewById(R.id.editContact)
val ivDelete: ImageView = itemView.findViewById(R.id.deleteContact)
}
4. Adapter: ContactAdapter.kt (có Filterable)
import android.app.Activity
import android.content.Context
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Filter
import android.widget.Filterable
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
class ContactAdapter(
private val context: Context,
private var contactList: MutableList
) : RecyclerView.Adapter(), Filterable {
private val originalList = contactList.toMutableList()
private val db = SqliteDatabase(context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.contact_list_layout, parent, false)
return ContactViewHolder(view)
}
override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
val contact = contactList[position]
holder.tvName.text = contact.name
holder.tvPhone.text = contact.phno
holder.ivEdit.setOnClickListener { showEditDialog(contact) }
holder.ivDelete.setOnClickListener {
db.deleteContact(contact.id)
refreshData()
}
}
override fun getItemCount() = contactList.size
private fun refreshData() {
contactList.clear()
contactList.addAll(db.getAllContacts())
notifyDataSetChanged()
}
private fun showEditDialog(contact: Contacts) {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.add_contacts, null)
val etName = view.findViewById(R.id.enterName)
val etPhone = view.findViewById(R.id.enterPhoneNum)
etName.setText(contact.name)
etPhone.setText(contact.phno)
AlertDialog.Builder(context)
.setTitle("Sửa liên hệ")
.setView(view)
.setPositiveButton("LƯU") { _, _ ->
val name = etName.text.toString().trim()
val phone = etPhone.text.toString().trim()
if (TextUtils.isEmpty(name)) {
Toast.makeText(context, "Tên không được rỗng", Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
contact.name = name
contact.phno = phone
db.updateContact(contact)
refreshData()
}
.setNegativeButton("HỦY", null)
.show()
}
// Filterable implementation
override fun getFilter(): Filter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val query = constraint?.toString()?.lowercase() ?: ""
val results = FilterResults()
if (query.isEmpty()) {
results.values = originalList
results.count = originalList.size
} else {
val filtered = originalList.filter { it.name.lowercase().contains(query) }
results.values = filtered
results.count = filtered.size
}
return results
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
contactList = (results?.values as? MutableList) ?: mutableListOf()
notifyDataSetChanged()
}
}
}
5. Layout Item: contact_list_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginHorizontal="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/contactName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/phoneNum"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textColor="@android:color/darker_gray" />
</LinearLayout>
<ImageView
android:id="@+id/editContact"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:contentDescription="Sửa"
android:src="@drawable/ic_baseline_edit_24" />
<ImageView
android:id="@+id/deleteContact"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:contentDescription="Xóa"
android:src="@drawable/ic_baseline_delete_24" />
</LinearLayout>
</androidx.cardview.widget.CardView>
6. Layout Dialog: add_contacts.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/enterName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Tên liên hệ"
android:inputType="text"
android:maxLines="1" />
<EditText
android:id="@+id/enterPhoneNum"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="Số điện thoại"
android:inputType="phone"
android:maxLines="1" />
</LinearLayout>
7. MainActivity.kt
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
private lateinit var db: SqliteDatabase
private lateinit var adapter: ContactAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
title = "Quản Lý Danh Bạ"
db = SqliteDatabase(this)
val recyclerView = findViewById(R.id.myContactList)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(true)
loadContacts()
findViewById
8. Layout Chính: activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/myContactList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="80dp"
android:paddingBottom="8dp" />
<Button
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:padding="12dp"
android:text="THÊM"
android:textAllCaps="false" />
</FrameLayout>
Chạy ứng dụng
Chạy app. Nhấn "THÊM" -> Nhập tên, số -> "THÊM". Danh sách hiển thị. Click icon sửa/xóa trên mỗi item. Gõ vào ô tìm kiếm (nếu thêm SearchView) để lọc.
Cách hiện đại: Room Database (Khuyến nghị)
SQLiteOpenHelper là API cấp thấp. Google khuyến khích dùng Room (ORM trên SQLite):
- Compile-time verification của SQL.
- LiveData/Flow/RxJava support tự động cập nhật UI.
- Coroutine support cho IO.
- Migration schema an toàn.
Ví dụ Room (Khái quát)
// Entity
@Entity(tableName = "contacts")
data class Contact(@PrimaryKey(autoGenerate = true) val id: Int, val name: String, val phone: String)
// DAO
@Dao
interface ContactDao {
@Insert fun insert(contact: Contact): Long
@Update fun update(contact: Contact): Int
@Delete fun delete(contact: Contact): Int
@Query("SELECT * FROM contacts ORDER BY name ASC") fun getAll(): LiveData>
@Query("SELECT * FROM contacts WHERE name LIKE :query") fun search(query: String): LiveData>
}
// Database
@Database(entities = [Contact::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun contactDao(): ContactDao
}
// Repository -> ViewModel -> Observer in Activity -> RecyclerView.Adapter.submitList()
Tóm tắt
Bài viết trình bày cách triển khai RecyclerView + SQLite CRUD đầy đủ. Đối với dự án thực tế, hãy sử dụng Room Database kết hợp ViewModel, LiveData/Flow, Coroutines để code sạch, bảo trì dễ, hiệu năng tốt.