๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿš€ Development/Android

[์•ˆ๋“œ๋กœ์ด๋“œ] Jetpack์˜ Room DB์ด๋ž€? (์‚ฌ์šฉ๋ฒ• ์˜ˆ์ œ)

by Jay Din 2023. 5. 17.
728x90
๋ฐ˜์‘ํ˜•

Room DB๋ž€?

Room์€ ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ์˜ ๋‚ด์žฅ DB์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค.

Jetpack ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘ ํ•˜๋‚˜๋กœ DB ๋ฐ์ดํ„ฐ๋ฅผ Java/Kotlin์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ORM ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

Room์€ SQLite์˜ ์ถ”์ƒ ๋ ˆ์ด์–ด ์œ„์— ์ œ๊ณตํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, SQLite์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์—ฌ ํŽธํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•œ๋‹ค.

LiveData๋‚˜ RxJava์™€ ๊ฐ™์ด Observation ํ˜•ํƒœ๋„ ์ง€์›ํ•˜๋ฏ€๋กœ ์•„ํ‚คํ…์ณ ํŒจํ„ด์—๋„ ์ ์šฉ์ด ๋งค์šฐ ์‰ฝ๋‹ค.

 

SharedPreferences์™€ ์ฐจ์ด

SharedPreferences๋„ ์•ฑ์˜ ๋กœ์ปฌ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ฐ€๋ฒผ์šด ๋ฐ์ดํ„ฐ๋ฅผ์ €์žฅํ•  ๋ชฉ์ ์œผ๋กœ ๋กœ์ปฌ DB๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

  • ์˜ˆ๋ฅผ๋“ค์–ด,
    ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•  ๋•Œ ์“ธ Stringํ˜• access_token์„ ์ €์žฅํ•  ๋•Œ

  Room DB๋Š” ํฐ ์‚ฌ์ด์ฆˆ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

 

Room ์‚ฌ์šฉ์ด ๊ถŒ์žฅ๋˜๋Š” ์ด์œ 

  1. Room์€ LiveData์™€ RxJava๋ฅผ ์œ„ํ•œObservation์œผ๋กœ ์ƒ์„ฑํ•˜์—ฌ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ SQLite๋Š” ๊ทธ๋ ‡์ง€ ์•Š๋‹ค.
  2. Room์€ SQLite์„ ํฌํ•จํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— SQLite๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์ด ํŽธ๋ฆฌํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•˜๋ฉฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜๋„ ๊ฐ„๋‹จํ•˜๋‹ค.
  3. SQLite์™€ ๋‹ฌ๋ฆฌ ์ปดํŒŒ์ผ ํƒ€์ž„์— ์ฟผ๋ฆฌ์˜ ์ ํ•ฉ์„ฑ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฆ‰, SQLite๋ณด๋‹ค ํŽธ๋ฆฌํ•œ DB๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

SQLite์— ๋Œ€ํ•œ ๋” ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์•„๋ž˜ ๋งํฌ ์ฐธ๊ณ 

https://jay-din.tistory.com/4

 

[์•ˆ๋“œ๋กœ์ด๋“œ] Local DB SQLite ์ด๋ž€? (์‚ฌ์šฉ๋ฒ• ์˜ˆ์ œ)

SQLite ๋ž€? SQLite๋Š” ์˜คํ”ˆ ์†Œ์Šค ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ ์‹œ์Šคํ…œ(RDBMS)์œผ๋กœ, ์ž‘๊ณ  ๊ฒฝ๋ นํ™”๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—”์ง„์ด๋‹ค. ์„œ๋ฒ„๊ฐ€ ์•„๋‹ˆ๋ผ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์— ๋„ฃ์–ด ์‚ฌ์šฉํ•˜๋Š” ๋น„๊ต์  ๊ฐ€๋ฒผ

jay-din.tistory.com

 

Room์˜ 3๊ฐ€์ง€ ๊ตฌ์„ฑ์š”์†Œ    

๋ฐ˜์‘ํ˜•

Entity

  • DB์—์„œ Table์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.
  • @Entity๋กœ ์„ ์–ธํ•œ๋‹ค.

Dao (Data Access Objects)

  • ๋ฐ์ดํ„ฐ์— Access ํ•  ์ˆ˜ ์žˆ๋Š” Interface
  • @Dao๋กœ ์„ ์–ธํ•œ๋‹ค.

Room DataBase

  • abstract class๋กœ ์„ ์–ธํ•˜๊ณ  Room Database๋ฅผ ์ƒ์† ๋ฐ›๋Š”๋‹ค.
  • @Database๋กœ ์„ ์–ธํ•œ๋‹ค.
  • DB์—์„œ ์‚ฌ์šฉ๋  ํ…Œ์ด๋ธ”์˜ ์ •๋ณด(entitiy)๊ฐ€ ํ•„์š”ํ•˜๊ณ , version ์ •๋ณด๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
@Database(entities = [TextEntity::class], version = 1)
  • App์ด ์—…๋ฐ์ดํŠธ ๋˜์–ด์„œ DB๊ฐ€ ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ ์ด์ „ DB์™€๊ตฌ๋ถ„์ด ํ•„์š”ํ•˜๋ฏ€๋กœ version ์— int ๊ฐ’์„ ๋„ฃ์–ด์„œ ๊ด€๋ฆฌํ•œ๋‹ค.

 

์‚ฌ์šฉ ์˜ˆ์ œ 

Build.gradle

id 'kotlin-kapt'
// ROOM
def roomVersion = "2.4.0"

implementation("androidx.room:room-runtime:$roomVersion")
annotationProcessor("androidx.room:room-compiler:$roomVersion")

// To use Kotlin annotation processing tool (kapt)
kapt("androidx.room:room-compiler:$roomVersion")

// Coroutine
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")

TextEntity.kt

  • Entity๋Š” ์‹ค์ฒด(๊ฐ์ฒด)์ด๋‹ค. ํ”ํžˆ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฐœ๋… ์Šคํ‚ค๋งˆ๋ฅผ ๋œปํ•˜๋ฉฐ ์‰ฝ๊ฒŒ ํ…Œ์ด๋ธ”์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค. 
  • data class๋ฅผ ์ •์˜ํ•˜๊ณ  @Entity ์–ด๋…ธํ…Œ์ด์…˜์„ ํ‘œ์‹œํ•œ๋‹ค.
  • Entity์—๋Š” ํ•˜๋‚˜ ์ด์ƒ์˜ ๊ธฐ๋ณธํ‚ค๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•˜๋ฉฐ, @PrimaryKey๋กœ ์„ ์–ธ๋œ ๋ณ€์ˆ˜๊ฐ€ ๊ธฐ๋ณธํ‚ค๋‹ค.
  • ๊ธฐ๋ณธํ‚ค๋Š” ๋ณตํ•ฉํ‚ค๋กœ ์ด๋ฃจ์–ด์งˆ ์ˆ˜ ์žˆ์œผ๋ฉฐ ์•„๋ž˜์™€ ๊ฐ™์ด ์–ด๋…ธํ…Œ์ด์…˜์—์„œ ์„ค์ •ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.
@Entity(primaryKeys = arrayOf("firstName", "lastName"))
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

// ํ…Œ์ด๋ธ” ์ด๋ฆ„ ์„ค์ •
@Entity(tableName = "text_table")
data class TextEntity(

    @PrimaryKey(autoGenerate = true)
    // @CcolumnInfo
    // ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ณ€์ˆ˜ ์ด๋ฆ„์ด ์—ด ์ด๋ฆ„์ด ๋œ๋‹ค. ๋งŒ์•ฝ ๋ณ„๋„๋กœ ์—ด ์ด๋ฆ„์„ ์„ค์ •ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด name ์†์„ฑ์„ ์ฃผ๋ฉด ๋œ๋‹ค
    @ColumnInfo(name = "id")
    var id: Int,
    @ColumnInfo(name = "text")
    var text: String

)

TextDao.kt

  • DAO ๋ฅผ ํ†ตํ•ด ์ฟผ๋ฆฌ๋ฌธ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.
  • DAO ๋กœ ์ •์˜ํ•˜๊ธฐ ์œ„ํ•ด์„  @Dao๋ผ๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ด ํ•„์š”ํ•˜๋ฉฐ, ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ž‘์„ฑ๋œ๋‹ค.
  • @Query ๋ฅผ ํ†ตํ•ด ์ง์ ‘ SQL์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ ์™ธ์—๋„ @Insert @Delete @Update ๋“ฑ์œผ๋กœ ๊ฐ„ํŽธํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.
  • ํ™œ์šฉ ๋ฐฉ๋ฒ•์ด ๋งŽ์œผ๋‹ˆ ์ž์„ธํ•œ๊ฑด ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ์ž ๋ฌธ์„œ๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค.
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface TextDao {

    @Query("SELECT * FROM text_table")
    fun getAllData(): List<TextEntity>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(text: TextEntity)

    @Query("DELETE FROM text_table")
    fun deleteAllData()

}

TextDatabase.kt   

Room ์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ •์˜ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” @Database ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ํด๋ž˜์Šค๋Š” ์ถ”์ƒ ํด๋ž˜์Šค๋กœ ์ž‘์„ฑ๋˜์–ด์•ผ ํ•œ๋‹ค.

์ถ”์ƒ ํด๋ž˜์Šค๋Š” RoomDatabse() ๋ฅผ ์ƒ์†ํ•ด์•ผ ํ•˜๋ฉฐ ๋งค๊ฐœ ๋ณ€์ˆ˜๊ฐ€ ์—†๋Š” ์ถ”์ƒ ๋ฉ”์„œ๋“œ๋ฅผ ํฌํ•จํ•ด์•ผ ํ•œ๋‹ค.

์–ด๋…ธํ…Œ์ด์…˜์—๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์—ฐ๊ฒฐ๋œ ํ•ญ๋ชฉ์˜ ๋ชฉ๋ก๊ณผ ๋ฒ„์ „์„ ํฌํ•จํ•ด์•ผ ํ•œ๋‹ค. 

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

@Database(entities = [TextEntity::class], version = 1)
abstract class TextDatabase : RoomDatabase() {

    abstract fun textDao() : TextDao

    companion object{
        @Volatile
        private var INSTANCE : TextDatabase? = null

        fun getDatabase(
            context : Context
        ) : TextDatabase{
            return INSTANCE ?: synchronized(this){
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    TextDatabase::class.java,
                    "text_database"
                )
                    .fallbackToDestructiveMigration()
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}
๋ฐ˜์‘ํ˜•

 

MainActivity.kt

์—ฌ๊ธฐ์„œ ์ฝ”๋ฃจํ‹ด์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๋Š” ๋ณด๋‹ค์‹œํ”ผ ๋™์ž‘์„ ํ•  ๋•Œ๋งˆ๋‹ค ๋กœ๊ทธ๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์žˆ๋‹ค.

์ด ๋•Œ ์ฝ”๋ฃจํ‹ด์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๋ฅผ  ๋งŒ๋‚˜๊ฒŒ ๋œ๋‹ค.

 java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

์›์ธ์€ ์•ˆ๋“œ๋กœ์ด๋“œ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ๊ฐ€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋ฅผ ์ ์œ ํ•  ๊ฒƒ์„ ์—ผ๋ คํ•˜์—ฌ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•  ๊ฒƒ์„ ๊ฐ•์š”ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

๋”ฐ๋ผ์„œ ์ฟผ๋ฆฌ๋ฅผ CoroutineScope ๋‚ด๋ถ€์—์„œ ์‹คํ–‰ํ•˜๋ฉด ์ฝ”๋ฃจํ‹ด์€ IO ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์—๋Ÿฌ๋ฅผ ํ”ผํ•  ์ˆ˜ ์žˆ๋‹ค.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class  MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val db = TextDatabase.getDatabase(this)

        val inputArea = findViewById<EditText>(R.id.textInputArea)
        val insertBtn = findViewById<Button>(R.id.insert)
        val getAllBtn = findViewById<Button>(R.id.getData)
        val deleteBtn = findViewById<Button>(R.id.delete)

		// ๋ฐ์ดํ„ฐ ์‚ฝ์ž…
        insertBtn.setOnClickListener {
            CoroutineScope(Dispatchers.IO).launch {
                db.textDao().insert(TextEntity(0, inputArea.text.toString()))
                Log.d("MainActivity", db.textDao().getAllData().toString())
                inputArea.setText("")
            }

        }

		// ๋ฐ์ดํ„ฐ ๋ชจ๋‘ ๊ฐ€์ ธ์˜ค๊ธฐ
        getAllBtn.setOnClickListener {
            CoroutineScope(Dispatchers.IO).launch {
                Log.d("MainActivity", db.textDao().getAllData().toString())
            }
        }

		// ๋ฐ์ดํ„ฐ ๋ชจ๋‘ ์‚ญ์ œ
        deleteBtn.setOnClickListener {
            CoroutineScope(Dispatchers.IO).launch {
                db.textDao().deleteAllData()
                Log.d("MainActivity", db.textDao().getAllData().toString())
            }
        }

    }

}

์‹คํ–‰ ๊ฒฐ๊ณผ

1. EditText์— hihi ์ž…๋ ฅํ•˜๊ณ  ์‚ฝ์ž…ํ•œ๋‹ค.

2. hihi ๋ฅผ ํ•œ ๋ฒˆ ๋” ์‚ฝ์ž…ํ•œ๋‹ค.

3. hello ๋ฅผ ์‚ฝ์ž…ํ•œ๋‹ค.

4. ๋ฐ์ดํ„ฐ๋ฅผ  ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ถœ๋ ฅ๋œ๋‹ค.

[TextEntity(id=1, text=hihi), TextEntity(id=2, text=hihi), TextEntity(id=3, text=hello)]

5. ๋ฐ์ดํ„ฐ ์‚ญ์ œ๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ถœ๋ ฅ๋˜์–ด ๊ฒฐ๊ณผ๋ฅผ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

[ ]

์ฐธ๊ณ 

https://velog.io/@limsaehyun/Android-Kotlin-Room-DB%EC%9D%98-%EC%82%AC%EC%9A%A9%EB%B2%95%EA%B3%BC-%EC%98%88%EC%A0%9C

 

Android Kotlin | Room DB์˜ ์‚ฌ์šฉ๋ฒ•๊ณผ ์˜ˆ์ œ

What is? Room์€ ์Šค๋งˆ์Šคํฐ ๋‚ด์žฅ DB์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ORM(Object Relational Mapping) ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ์‰ฝ๊ฒŒ ๋งํ•ด์„œ. ROOM์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๊ฐ์ฒด๋ฅผ ์ž๋ฐ” or ์ฝ”ํ‹€๋ฆฐ์˜ ๊ฐ์ฒด๋กœ ๋งคํ•‘ํ•ด์ฃผ๋Š” ์—ญ

velog.io

https://kong-droid.com/41

 

[Kotlin/Android] Room ์‚ฌ์šฉํ•˜๊ธฐ

๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์ธ Room์„ ์‚ฌ์šฉํ•ด๋ณด๋‹ค๊ฐ€ ๊พธ์ค€ํžˆ ์‚ฌ์šฉํ•˜๊ฒŒ ๋  DB์ผ ๊ฒƒ ๊ฐ™์•„ ์ •๋ฆฌํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค Room์€ ๊ธฐ๋ณธ ๊ฐœ๋…์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์•„์ฃผ ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋กœ ์ง„ํ–‰ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. [2022.06

kong-droid.com

https://latte-is-horse.tistory.com/155

 

[Kotlin] Room ์‚ฌ์šฉ๋ฒ• (Android์—์„œ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉํ•˜๊ธฐ)

Room์€ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฐœ๋ฐœ ์ค‘ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ ์ž ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. ๋งŒ์•ฝ ๊ธฐ๊ธฐ๊ฐ€ ๋„คํŠธ์›Œํฌ์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†์„ ๋•Œ ์˜คํ”„๋ผ์ธ ์ƒํƒœ์ธ ๋™์•ˆ์—๋„ ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ์ „ํžˆ ์ฝ˜ํ…์ธ ๋ฅผ ํƒ์ƒ‰

latte-is-horse.tistory.com

https://hydroponicglass.tistory.com/entry/Android-Kotlin-Cannot-access-database-on-the-main-thread-since-it-may-potentially-lock-the-UI-for-a-long-period-of-time

728x90
๋ฐ˜์‘ํ˜•