일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Android
- 코틀린
- CustomView
- Behavior
- Coroutine
- AppBarLayout
- 백준
- onLayout
- recyclerview
- HTTP
- View
- room
- sqlite
- CoordinatorLayout
- notification
- BOJ
- lifecycle
- 안드로이드
- Algorithm
- LiveData
- 알고리즘
- DataBinding
- hilt
- ViewModel
- CollapsingToolbarLayout
- 알림
- onMeasure
- kotlin
- Navigation
- activity
- Today
- Total
개발일지
Android in A..Z - Room (기본) 본문
Room
Room이란 SQLite를 쉽게 사용할 수 있도록 제공하는 추상레이어 라이브러리이다. Room을 사용하여 Android에 Database구현를 쉽고 빠르게 할 수 있다.
Room은 기본적으로 LiveData, Optional, Cursor 등의 강력한 기능도 지원한다.
dependencies {
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
기본 구조
- Room Database : DAO를 얻을 수 있다.
- DAO : 실제 데이터베이스에서 Entity에 대한 Select, Update, Delete 등의 작업을 수행한다.
- Entities : 데이터베이스에 저장된 데이터를 저장하는 클래스이다.
RoomDatabase
@Database 선언과 필요한 내용을 기제하고, abstract class로 선언후에 RoomDatabase를 상속받아 abstract fun으로 DAO를 획득하는 함수를 선언한다.
* SingleTon Pattern을 사용하면 관리하기 편하다.
AppDatabase
@Database(entities = [Drawer::class, ToDo::class], version = 2, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
companion object {
private var instance: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return instance ?: synchronized(this) {
Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
CoroutineScope(Dispatchers.IO).launch {
getInstance(context).drawer().insert(
Drawer(name = "ToDo")
)
}
}
})
.addMigrations(MIGRATION_1_2)
.build()
}.also {
instance = it
}
}
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE ToDo ADD COLUMN isFinished INTEGER DEFAULT 0 NOT NULL")
}
}
}
abstract fun drawer(): DrawerDao
abstract fun todo(): ToDoDao
}
- @Database : Room에 Database를 알리는 어노테이션이다.
- entities : Database에서 사용하는 Entity들을 나열한다.
- version : Database의 버전을 적는다. (Database Scheme가 변경될 경우 버전을 바꾸고 Migration을 해야한다.
- abstract fun을 선언하여 DAO를 얻을 수 있다.
DAO
기본적으로 interface로 선언하여 사용하고 insert, update, delete는 room에서 제공하는 에노테이션을 사용하고, select쿼리는 @Query 에노테이션을 사용하여 SQL을 작성한다.
* BaseDao를 만들어서 insert, delete, update에 대한 중복 코드를 줄일 수 있다.
* suspend fun으로 선언하여 사용하면 실수로 main thread에서 호출하는 것을 막을 수 있다.
BaseDao
@Dao
interface BaseDao<E: Any> {
@Transaction
@Insert
suspend fun insert(vararg elements: E)
@Transaction
@Insert
suspend fun insert(elements: List<E>)
@Transaction
@Delete
suspend fun delete(vararg elements: E)
@Transaction
@Delete
suspend fun delete(elements: List<E>)
@Transaction
@Update
suspend fun update(vararg elements: E)
@Transaction
@Update
suspend fun update(elements: List<E>)
}
- @Transaction : 데이터베이스에 Transaction과 같은 기능(SQL을 수행중에 Exception이 발생하면 롤백한다.)
- @Insert : Insert문이다.
- @Delete : Delete문이다. (매개변수의 PrimaryKey를 통해서 일치하는 데이터를 delete한다.)
- @Update : Update이다. (매개변수의 PrimaryKey를 통해서 일치하는 데이터를 update한다.)
DrawerDao
@Dao
interface DrawerDao : BaseDao<Drawer> {
@Query("SELECT * FROM Drawer")
fun findLiveData(): LiveData<MutableList<Drawer>>
@Query("SELECT * FROM Drawer")
fun findLiveDataWithToDo(): LiveData<MutableList<DrawerWithToDo>>
}
- @Query : SQL문을 작성하여 결과 값을 받을 수 있다. (결과 값으로 List, LiveData, Object, Optional, Cursor등 다양한 형태로 받을 수 있다.)
ToDoDao
@Dao
interface ToDoDao : BaseDao<ToDo> {
@Transaction
@Query("SELECT * FROM ToDo WHERE drawerName = :drawerName")
fun findLiveDataByDrawerName(drawerName: String): LiveData<MutableList<ToDo>>
}
Entity
@Entity 선언과 @PrimaryKey 선언을 사용하여 Entity로 설정할 수 있다.
Drawer
@Entity(
indices = [
Index(value = ["name"], unique = true)
]
)
data class Drawer(
@PrimaryKey(autoGenerate = true)
var id: Long = 0L,
var name: String = "",
)
- @Entity : Room에 Entity 선언을 알린다.
- indices : Database에 Index를 설정할 수 있다. (unique로 고유한 값을 가지는지 설정할 수 있다.)
- @PrimaryKey : Entity의 PK를 설정한다. (여러개의 PK를 설정할 때는 @Entity 에노테이션 안에서 설정한다.)
- autoGenerate : AUTO INCREAMENT와 같은 기능
ToDo
@Entity(
foreignKeys = [
ForeignKey(entity = Drawer::class, parentColumns = ["name"], childColumns = ["drawerName"], onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE)
]
)
data class ToDo(
@PrimaryKey(autoGenerate = true)
var id: Long = 0L,
var text: String = "",
var drawerName: String = "",
var isFinished: Boolean = false
)
- foreignKeys : Entity의 FK를 설정한다.
- entity : 관계를 설정할 Entity
- parentColumns : FK를 받을 Column들을 나열한다.
- childColumns : FK로 받는 Column들을 나열한다.
- onDelete, onUpdate : FK의 제약사항을 설정한다. (CASCADE -> 같이 변경된다.)
사용
Room을 Main Thread에서 접근하면 Error가 발생하기 때문에 새로운 Thread나 Coroutine을 활용한다.
AppDatabase의 instance를 얻고 DAO를 사용하여 데이터베이스에 접근한다.
DrawerFragment
class DrawerFragment : BaseFragment<FragmentDrawerBinding>(R.layout.fragment_drawer) {
init {
setHasOptionsMenu(true)
}
companion object {
private const val MENU_DELETE = 1000
}
private lateinit var menu: Menu
private val drawerWithToDoAdapter by lazy { DrawerWithToDoAdapter() }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
AppDatabase.getInstance(requireContext()).drawer().findLiveDataWithToDo().observe(viewLifecycleOwner) {
drawerWithToDoAdapter.submitList(it)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
this.menu = menu
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
MENU_DELETE -> {
drawerWithToDoAdapter.tracker?.let {
CoroutineScope(Dispatchers.IO).launch {
val deleteList = ArrayList<Drawer>(it.selection.size())
it.selection.forEach { id ->
deleteList.add(Drawer(id = id))
}
AppDatabase.getInstance(requireContext()).drawer().delete(deleteList)
withContext(Dispatchers.Main) {
it.clearSelection()
}
}
}
}
}
return super.onOptionsItemSelected(item)
}
override fun init() {
super.init()
initRecyclerView()
initSelectionTracker()
initOnAddButton()
}
private fun initRecyclerView() {
with(binding.recyclerView) {
adapter = drawerWithToDoAdapter
addItemDecoration(GridSpacingItemDecoration(2, 10.toDp()))
}
}
private fun initSelectionTracker() {
with(binding.recyclerView) {
drawerWithToDoAdapter.tracker = SelectionTracker.Builder(
"Selection",
this,
DrawerWithToDoAdapter.DrawerWithToDoKeyProvider(this),
DrawerWithToDoAdapter.DrawerWithToDoDetailsLookup(this),
StorageStrategy.createLongStorage()
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build().apply {
addObserver(object : SelectionTracker.SelectionObserver<Long>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
val tracker = this@apply
if (tracker.hasSelection() && menu.findItem(MENU_DELETE) == null) {
menu.add(Menu.NONE, MENU_DELETE, Menu.NONE, "Delete")
.setIcon(R.drawable.ic_delete)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} else if (!tracker.hasSelection() && menu.findItem(MENU_DELETE) != null) {
menu.removeItem(MENU_DELETE)
}
}
})
}
}
}
private fun initOnAddButton() {
binding.setOnAdd {
with(binding.text) {
val name = text.trim()
if (name.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
AppDatabase.getInstance(requireContext()).drawer().insert(Drawer(name = name.toString()))
withContext(Dispatchers.Main) {
binding.text.text.clear()
}
}
}
}
}
}
}
ToDoFragment
class ToDoFragment : BaseFragment<FragmentTodoBinding>(R.layout.fragment_todo) {
init {
setHasOptionsMenu(true)
}
private val args by navArgs<ToDoFragmentArgs>()
private val drawerName by lazy { args.drawerName }
private val todoAdapter by lazy { ToDoAdapter() }
private var filter by Delegates.observable(false) { _, _, _ ->
submitList()
}
private var currentList = emptyList<ToDo>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
AppDatabase.getInstance(requireContext()).todo().findLiveDataByDrawerName(drawerName).observe(viewLifecycleOwner) {
currentList = it
submitList()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_todo_fragment, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
R.id.finished -> {
filter = true
}
R.id.notFinished -> {
filter = false
}
}
return super.onOptionsItemSelected(item)
}
private fun submitList() {
todoAdapter.submitList(currentList.filter { it.isFinished == filter })
}
override fun init() {
super.init()
initToolbar()
initRecyclerView()
initOnAddButton()
}
private fun initToolbar() {
(activity as AppCompatActivity).supportActionBar?.title = "$drawerName Drawer"
}
private fun initRecyclerView() {
with(binding.recyclerView) {
adapter = todoAdapter
addItemDecoration(GridSpacingItemDecoration(1, 10.toDp()))
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return viewHolder is ToDoAdapter.ToDoHolder
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (viewHolder is ToDoAdapter.ToDoHolder) {
if (!viewHolder.element.isFinished) {
CoroutineScope(Dispatchers.IO).launch {
viewHolder.element.isFinished = true
AppDatabase.getInstance(requireContext()).todo().update(viewHolder.element)
}
} else {
CoroutineScope(Dispatchers.IO).launch {
when(direction) {
ItemTouchHelper.RIGHT -> {
AppDatabase.getInstance(requireContext()).todo().delete(viewHolder.element)
}
ItemTouchHelper.LEFT -> {
viewHolder.element.isFinished = false
AppDatabase.getInstance(requireContext()).todo().update(viewHolder.element)
}
}
}
}
}
}
}).attachToRecyclerView(this)
}
}
private fun initOnAddButton() {
binding.setOnAdd {
with(binding.text) {
val name = text.trim()
if (name.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
AppDatabase.getInstance(requireContext()).todo().insert(ToDo(text = name.toString(), drawerName = drawerName))
withContext(Dispatchers.Main) {
binding.text.text.clear()
}
}
}
}
}
}
}
Git (예제코드)
github.com/KangTaeJong98/Example/tree/main/Android/Room
'Android (안드로이드) > Room' 카테고리의 다른 글
Android in A..Z - Room (TypeConvert) (1) | 2021.01.17 |
---|---|
Android in A..Z - Room (allowMainThreadQueries) (0) | 2021.01.17 |
Android in A..Z - Room (관계) (0) | 2021.01.17 |
Android in A..Z - Room (Migration) (0) | 2021.01.17 |
Android in A..Z - Room (데이터 미리 채우기) (0) | 2021.01.17 |