Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Cách sử dụng RecyclerView với Database (SQLite) trên Android bằng Kotlin

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.