[Must Have Joyce의 안드로이드 앱프로그래밍] 10장 할 일 리스트: Room 데이터베이스, 리사이클러뷰

알린·2024년 1월 28일
0

Room 지속성 라이브러리

  • SQLite에 추상화 계층을 추가한 라이브러리
  • 스마트폰 내장 데이터베이스에 데이터를 쉽게 저장할 수 있음
  • 서버로부터 받은 데이터를 캐싱해 저장
    👉 네트워크 없이도 데이터 열람 가능

엔티티

  • 데이터베이스 테이블 (관련된 속성의 집합)

데이터 접근 객체

  • 데이터베이스에 접근하는 함수를 제공하는 인터페이스

룸 데이터베이스

  • 테이블 정의
  • 테이블 버전 명시
  • RoomDatabase 클래스를 상속받는 추상 클래스

Room 라이브러리 추가

  1. 모듈 수준 build.gradle 파일 내 다음 코드 추가
plugins {
    id("org.jetbrains.kotlin.kapt")
}
dependencies {
    val room_version = "2.5.0"

    implementation("androidx.room:room-runtime:$room_version")
    // To use Kotlin annotation processing tool (kapt)
    kapt("androidx.room:room-compiler:$room_version")
}
  1. 상단의 [Sync Now] 클릭

👉 Ropm 사용하기 Android 공식 문서

데이터베이스 구현

데이터베이스 구성

엔티티: ToDoEntity
데이터 접근 객체: ToDoDao
룸 데이터베이스: AppDatabase

엔티티 생성

  • 데이터 구조 정의
  1. [app] ➡️ [kotlin+java] ➡️ [com.example.todolistapp] ➡️ 마우스 우클릭 ➡️ [New] ➡️ [Package] ➡️ db 패키지 생성

  2. [db] ➡️ [New] ➡️ [Kotlin class/File] ➡️ ToDoEntity 생성

  3. ToDoEntity에 다음 코드 작성

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity  // 구성요소 알려주는 어노테이션 필수
data class ToDoEntity (
    // 기본키 => 각 정보를 식별하는 값
    // autoGenerate = true => id가 자동으로 1씩 증가되며 저장
    @PrimaryKey(autoGenerate = true) var id : Int? = null,
    @ColumnInfo(name = "title") val title : String,
    @ColumnInfo(name = "importance") val importance : Int
)

👉 데이터 스키마 정리 Android 공식 문서

DAO 생성

  • 데이터로 무엇을 할지 정의
  1. [db] ➡️ 아무스 우클릭 ➡️ [New] ➡️ [Kotlin class/File] ➡️ [Interface] ➡️ ToDoDao 생성
  2. ToDoDao에 다음 코드 작성
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query

@Dao
interface ToDoDao {
    // 생성한 ToDoEntity에서 모든 데이터를 불러오는 쿼리 함수
    @Query("select * from ToDoEntity")
    fun getAll() : List<ToDoEntity>

    //ToDoEntity 객체를 테이블에 삽입
    @Insert
    fun insertTodo(todo: ToDoEntity)

    // 특정 ToDoEntity 객체를 테이블에서 삭제
    @Delete
    fun deleteTodo(todo: ToDoEntity)
}

데이터베이스 생성

데이터베이스 클래스 만족 조건
1. @Database 어노테이션에서 데이터베이스와 관련된 모든 Entity를 나열
2. RoomDatabase를 상속하는 추상 클래스
3. DAO를 반환하고 인수가 존재하지 않는 추상 함수가 있음

  1. [db] ➡️ 아무스 우클릭 ➡️ [New] ➡️ [Kotlin class/File] ➡️ AppDatabase 생성

  2. AppDatabase에 다음 코드 작성

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = arrayOf(ToDoEntity::class), version = 1)  // 조건 1
abstract class AppDatabase : RoomDatabase() {  // 조건 2
    abstract fun getTodoDao() : ToDoDao  // 조건 3

    companion object {
        val databaseName = "db_todo"  // 데이터베이스 명
        var appDatabase : AppDatabase? = null

        fun getInstance(context : Context) : AppDatabase? {  // 싱글턴 패턴 함수 구현
            if (appDatabase == null) {  
                // appDatabase가 null일 때 객체 생성
                appDatabase = Room.databaseBuilder(context, AppDatabase::class.java, databaseName).build()
            }
            // null이 아니면 기존 객체 반환
            return appDatabase
        }
    }
}

💡 싱글턴 패턴

  • 데이터베이스를 쓸 때마다 객체를 생성하기보다는 한 객체를 여러 클래스에서 글로벌하게 쓰는게 더 효율적
    👉 싱글턴 패턴

데이터베이스 사용

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.todolistapp.databinding.ActivityAddTodoBinding
import com.example.todolistapp.db.AppDatabase
import com.example.todolistapp.db.ToDoDao
import com.example.todolistapp.db.ToDoEntity

class AddTodoActivity : AppCompatActivity() {
    lateinit var binding: ActivityAddTodoBinding
    lateinit var db : AppDatabase
    lateinit var todoDao: ToDoDao  // ToDoDao인터페이스 => insert, delete, query 기능 제공
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityAddTodoBinding.inflate(layoutInflater)
        setContentView(binding.root)

        db = AppDatabase.getInstance(this)!!
        todoDao = db.getTodoDao()

        binding.btnCompletion.setOnClickListener {
            insertTodo()
        }
    }

    private fun insertTodo() {
        val todoTitle = binding.editTitle.text.toString()  // 할 일의 제목
        var todoImportance = binding.radioGroup.checkedRadioButtonId  // 할 일의 중요도

        when(todoImportance) {
            R.id.btn_high -> {
                todoImportance = 1
            }
            R.id.btn_middle -> {
                todoImportance = 2
            }
            R.id.btn_low -> {
                todoImportance = 3
            }
            else -> {
                todoImportance = -1
            }
        }

        // 중요도가 선택되지 않았거나, 제목이 작성되지 않았는지 체크
        if (todoImportance == -1 || todoTitle.isBlank()) {
            Toast.makeText(this, "모든 항목을 채워주세요.", Toast.LENGTH_SHORT).show()
        } else {
            // 백그라운드 스레드 실행
            Thread {
                todoDao.insertTodo(ToDoEntity(null, todoTitle, todoImportance))
                runOnUiThread {
                    Toast.makeText(this, "추가되었습니다.", Toast.LENGTH_SHORT) .show()
                    finish()
                }
            }
        }
    }
}

🚨 데이터베이스 관련 작업 시 주의점

  • 작업량이 큰 데이터베이스 관련 작업은 반드시 백그라운드 스레드에서 진행
  • 이를 실무에서는 Coroutines, RxJava, RxKotlin과 같은 라이브러리 사용(비동기 처리)

리사이클러뷰

  • 리스트뷰에 성능과 유연성을 더한 진화체

어댑터

  • 아이템뷰를 생성하고 데이터를 바인딩

레이아웃 매니저

  • 어댑터에 생성한 아이템뷰의 배치를 결정

    선형: LinearLayoutManager
    격자형: GridLayoutManager
    지그재그 격자형: StaggeredGridLayoutManager

리사이클러뷰 구현

어댑터 구현

  • 아이템뷰를 생성하고, 데이터를 뷰와 바인딩해줌

item_todo.xml

<?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">

    <TextView
        android:id="@+id/tv_importance"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginStart="20dp"
        android:background="@color/yellow"
        android:gravity="center"
        android:text="2"
        android:textColor="@color/white"
        android:textSize="19sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="20dp"
        android:text="GO TO GYM"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/tv_importance"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.todolistapp.databinding.ActivityAddTodoBinding
import com.example.todolistapp.databinding.ItemTodoBinding
import com.example.todolistapp.db.ToDoEntity

class ToDoRecyclerViewAdapter (private val todoList: ArrayList<ToDoEntity>) : // 어댑터 객체를 생성할 때 todoList를 인수로 받아줌
    //  RecyclerView.Adapter<MyViewHolder클래스>를 상속 => 뷰홀더 패턴
    RecyclerView.Adapter<ToDoRecyclerViewAdapter.MyViewHolder>() {

        // MyViewHolder 클래스 생성
        inner class MyViewHolder(binding : ItemTodoBinding) : RecyclerView.ViewHolder(binding.root) {
            val tv_importance = binding.tvImportance
            val tv_title = binding.tvTitle
            val root = binding.root
        }

    // MyViewHolder 객체 생성
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        // item_todo.xml 뷰 바인딩 객체 생성
        val binding: ItemTodoBinding = ItemTodoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return MyViewHolder(binding)
    }

    // 받아온 데이터를 MyViewHolder객체에 어떻게 넣어줄지 결정
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val todoData = todoList[position]
        // 중요도에 따라 색상 변경
        when (todoData.importance) {
            1 -> {
                holder.tv_importance.setBackgroundResource(R.color.red)
            }
            2 -> {
                holder.tv_importance.setBackgroundResource(R.color.yellow)
            }
            3 -> {
                holder.tv_importance.setBackgroundResource(R.color.green)
            }
        }
        // 중요도에 따라 중요도 텍스트 변경
        holder.tv_importance.text = todoData.importance.toString()
        // 할 일의 제목 변경
        holder.tv_title.text = todoData.title
    }

    // 데이터가 몇 개인지 반환
    override fun getItemCount(): Int {
        return  todoList.size
    }
}

💡 뷰홀더 패턴

  • 각 뷰 객체를 뷰 홀더에 보관하여 반복적인 메서드 호출을 줄여 속도를 개선하는 패턴
  • 뷰 홀더 클래스를 만들고, 안에 변수를 선언하면 뷰에 즉시 액세스가 가능해짐

MainActivity.kt에 어댑터 연결

  • 다음 코드 작성
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.todolistapp.databinding.ActivityMainBinding
import com.example.todolistapp.db.AppDatabase
import com.example.todolistapp.db.ToDoDao
import com.example.todolistapp.db.ToDoEntity

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    private lateinit var db : AppDatabase
    private lateinit var toDoDao: ToDoDao
    private lateinit var todoList: ArrayList<ToDoEntity>
    private lateinit var adapter: ToDoRecyclerViewAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.btnAdd.setOnClickListener {
            val intent = Intent(this, AddTodoActivity::class.java)
            startActivity(intent)
        }

        // DB 인스턴스를 가져오고 DB작업을 할 수 있는 DAO를 가져옴
        db = AppDatabase.getInstance(this)!!
        toDoDao = db.getTodoDao()

        getAllTodoList()
    }

    private fun getAllTodoList() {
        // 백그라운드에서 실행
        Thread {
            todoList = ArrayList(toDoDao.getAll())
            setRecyclerView()
        }.start()
    }

    private fun setRecyclerView() {
        // UI 작업이므로 UI 스레드에서 실행
        runOnUiThread {
            adapter = ToDoRecyclerViewAdapter(todoList)  // 어댑터 객체 할당
            // 리사이클러뷰 어댑터로 위에서 만든 어댑터 설정
            binding.recyclerView.adapter = adapter
            // 레이아웃 매니저 설정
            binding.recyclerView.layoutManager = LinearLayoutManager(this)
        }
    }

    // 액티비티가 멈췄다가 다시 시작되었을 때 실행되는 함수(생명주기 관련)
    override fun onRestart() {
        super.onRestart()
        getAllTodoList()
    }
}

구현 화면


알림창(AlertDialog)

  • 진행하기 전에 사용자에게 의사를 물어볼 때 사용
  • title, message, positive button, negative button으로 구성
  • 각 버튼에 리스너 달기 가능
  1. [com.example.todolistapp] 아래OnItemLongClickListener 인터페이스 생성 후 다음 코드 작성
interface OnItemLongClickListener {
    // 길게 클릭되었을 때 실행
    fun onLongClick(position: Int)
}
  1. 메인 액티비티에서 ToDoRecyclerViewAdapter로 OnItemLongClickListener 인터페이스 구현체 넘겨주는 코드 작성,
    할 일이 길게 클릭되었을 때 리스너 함수 실행 함수 코드 작성
class ToDoRecyclerViewAdapter (private val todoList: ArrayList<ToDoEntity>, private val listener: OnItemLongClickListener) :  // OnItemLongClickListener인터페이스 구현체 넘겨주기
    RecyclerView.Adapter<ToDoRecyclerViewAdapter.MyViewHolder>() {
        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val todoData = todoList[position]
        // 중요도에 따라 색상 변경
        when (todoData.importance) {
            1 -> {
                holder.tv_importance.setBackgroundResource(R.color.red)
            }
            2 -> {
                holder.tv_importance.setBackgroundResource(R.color.yellow)
            }
            3 -> {
                holder.tv_importance.setBackgroundResource(R.color.green)
            }
        }
        // 중요도에 따라 중요도 텍스트 변경
        holder.tv_importance.text = todoData.importance.toString()
        // 할 일의 제목 변경
        holder.tv_title.text = todoData.title

        // 할 일이 길게 클릭되었을 때 리스너 함수 실행
        holder.root.setOnClickListener {  // holder.root => 한 아이템뷰의 루트 레이아웃
            listener.onLongClick(position)
            false
        }
    }
}
  1. 메안 액티비티에서 인터페이스 선언 해준 뒤, setRecyclerView() 함수에서 어댑터 객체 할당한 뒤,
    다음의 두 함수 추가 구현
    override fun onLongClick(position: Int) {
        val builder: AlertDialog.Builder = AlertDialog.Builder(this)
        builder.setTitle("할 일 삭제")
        builder.setMessage("정말 삭제하시겠습니다?")
        builder.setNegativeButton("취소", null)
        builder.setPositiveButton("네",
            object : DialogInterface.OnClickListener {
                override fun onClick(dialog: DialogInterface?, which: Int) {
                    deleteTodo(position)
                }
            })
        builder.show()
    }

    private fun deleteTodo(position: Int) {
        Thread {
            toDoDao.deleteTodo(todoList[position])  // DB에서 삭제
            todoList.removeAt(position)  // 리스트에서 삭제
            runOnUiThread {   // UI 관련 작업은 UI 스레드에
                adapter.notifyDataSetChanged()  // 어댑터에게 데이터가 바뀌었음을 알려줌 => 리사이클러뷰가 그에 맞춰 자동 업데이트
                Toast.makeText(this, "삭제되었습니다.", Toast.LENGTH_SHORT).show()
            }
        }.start()
    }

구현 결과

profile
Android 짱이 되고싶은 개발 기록 (+ ios도 조금씩,,👩🏻‍💻)

0개의 댓글