[Android] MVVM+Room Code

minnieยท2022๋…„ 1์›” 13์ผ
0

Jetpack

๋ชฉ๋ก ๋ณด๊ธฐ
8/13
post-thumbnail

๐ŸŒžMVVMํŒจํ„ด๊ณผ Room ์‚ฌ์šฉํ•˜์—ฌ ์ฃผ์†Œ๋ก ๋งŒ๋“ค๊ธฐ (Kotlin)

1. ์šฐ์„  ViewModel๊ณผ Room์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ข…์†์„ฑ ์ถ”๊ฐ€

// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.1'


//Room
implementation 'androidx.room:room-runtime:2.4.1'
kapt 'androidx.room:room-compiler:2.4.1'
implementation "androidx.room:room-ktx:2.4.1"

2. Room์„ ์ด์šฉํ•ด DB ์ƒ์„ฑ ๋ฐ ์‚ฝ์ž…, ์‚ญ์ œ

Entity : Contact.kt

Entity(๊ฐœ์ฒด)๋Š” ๊ด€๋ จ์ด ์žˆ๋Š” ์†์„ฑ๋“ค์ด ๋ชจ์—ฌ ํ•˜๋‚˜์˜ ์ •๋ณด ๋‹จ์œ„๋ฅผ ์ด๋ฃฌ ๊ฒƒ์œผ๋กœ Database๋‚ด์— ํ…Œ์ด๋ธ”์„ ํด๋ž˜์Šค๋กœ ๋‚˜ํƒ€๋‚ด์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.
@Entity๋ฅผ ํ†ตํ•ด ์ƒ์„ฑํ•ด์ฃผ๊ณ  ๊ธฐ๋ณธ์ ์œผ๋กœ ํด๋ž˜์Šค ์ด๋ฆ„์ด ํ…Œ์ด๋ธ” ์ด๋ฆ„์ด ๋˜์ง€๋งŒ ๋ณ„๋„๋กœ ์„ค์ •์„ ํ•ด์ฃผ๊ณ  ์‹ถ๋‹ค๋ฉด
@Entity์˜†์— (tableName = "ํ…Œ์ด๋ธ” ์ด๋ฆ„")์„ ํ†ตํ•ด ์ •ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.
@PrimaryKey๋Š” ํ‚ค ๊ฐ’์ด๊ธฐ ๋•Œ๋ฌธ์— ์œ ์ผํ•œ(Unique) ๊ฐ’์ด์–ด์•ผ ํ•œ๋‹ค. ์ง์ ‘ ์ง€์ •ํ•ด๋„ ๋˜์ง€๋งŒ autoGenerate๋ฅผ true๋กœ ์ฃผ๋ฉด ์ž๋™์œผ๋กœ ๊ฐ’์„ ์ƒ์„ฑํ•œ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๋”ฐ๋กœ ์นผ๋Ÿผ๋ช…์„ ์ง€์ •ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด @ColumnInfo(name="์นผ๋Ÿผ๋ช…")์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

@Entity(tableName = "contact")
data class Contact(
    @PrimaryKey(autoGenerate = true) // ๊ธฐ๋ณธํ‚ค๋กœ ์ง€์ • , null์ผ ๊ฒฝ์šฐ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋„๋ก autoGenerate ์†์„ฑ
    var id:Int?,
    var name:String,
    var number:String,
    var initial :Char,
    
    @ColumnInfo(name="cate")
    var category : Int? =0

){
    constructor() : this(null,"","",'\u0000',0)
}

DAO : ContactDao.kt

DAO๋Š” Data Access Object์˜ ์ค„์ž„๋ง๋กœ, ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•ด๋†“์€ ์ธํ„ฐํŽ˜์ด์Šค์ด๋‹ค.
@Dao ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ด๊ณ  ๊ทธ ์•ˆ์— ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.

DAO ์†์„ฑ์„ค๋ช…
@Insert๋ฐ์ดํ„ฐ ์‚ฝ์ž…
@Delete๋ฐ์ดํ„ฐ ์‚ญ์ œ
@Update๋ฐ์ดํ„ฐ ์ˆ˜์ •

์ด ์™ธ์— ์›ํ•˜๋Š” ๋ช…๋ น์„ ํ•˜๊ณ ์‹ถ๋‹ค๋ฉด @Query("์›ํ•˜๋Š” Sql๋ฌธ")์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

@Insert๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ ํ•  ๋•Œ ๊ฐ™์€ primary key ๊ฐ’์„ ๊ฐ€์ง„ ๊ฐ์ฒด๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ถฉ๋Œ์ฒ˜๋ฆฌ๋ฐฉ์‹์„ ํ†ตํ•ด ์—๋Ÿฌ๊ฐ€ ๋‚˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ถฉ๋Œ์ฒ˜๋ฆฌ๋ฐฉ์‹์„ค๋ช…
OnConflictStrategy.ABORT์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ ์ค‘๋‹จ
OnConflictStrategy.FAIL์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ ์‹คํŒจ ์ฒ˜๋ฆฌ
OnConflictStrategy.IGNORE์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ ๋ฌด์‹œ
OnConflictStrategy.REPLACE์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ ๋ฎ์–ด์“ฐ๊ธฐ
OnConflictStrategy.ROLLBACK์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ ์ด์ „์œผ๋กœ ๋˜๋Œ๋ฆฌ๊ธฐ
@Dao
interface ContactDao {
    @Query("SELECT * FROM contact ORDER BY name ASC")
    fun getAll() : LiveData<List<Contact>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(contact: Contact)

    @Delete
    fun delete(contact: Contact)
}

Database : ContactDatabase.kt

@Database ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์ž„์„ ํ‘œ์‹œํ•œ๋‹ค.
RoomDatabase ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ์ฒด ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ ์ถ”์ƒ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด ์ค˜์•ผ ํ•œ๋‹ค.

@Database(entities=[Entity ๋ช…::class], version=๋ฒ„์ „์ •๋ณด)

version์€ Entity์˜ ๊ตฌ์กฐ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•˜๋Š” ์ผ์ด ์ƒ๊ฒผ์„ ๋•Œ ์ด์ „ ๊ตฌ์กฐ์™€ ํ˜„์žฌ ๊ตฌ์กฐ๋ฅผ ๊ตฌ๋ถ„ํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. ์ฒ˜์Œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ƒํ™ฉ์ด๋ผ๋ฉด ๊ทธ๋ƒฅ 1์„ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค. ๋งŒ์•ฝ ๊ตฌ์กฐ๊ฐ€ ๋ฐ”๋€Œ์—ˆ๋Š”๋ฐ ๋ฒ„์ „์ด ๊ฐ™๋‹ค๋ฉด ์—๋Ÿฌ๊ฐ€ ๋œจ๋ฉฐ ๋””๋ฒ„๊น…์ด ๋˜์ง€ ์•Š๋Š”๋‹ค.

@Database(entities=arrayOf(Entity ๋ช…::class, Entity ๋ช…::class...), version=๋ฒ„์ „์ •๋ณด)

์—ฌ๋Ÿฌ๊ฐœ์˜ Entity๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์œ„์™€ ๊ฐ™์ด ๋ฐฐ์—ด ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

@Database(entities = [Contact::class],version = 1)
abstract class ContactDatabase : RoomDatabase() {
    abstract fun contactDao(): ContactDao
    companion object{
        private var INSTANCE : ContactDatabase? = null

        fun getInstance(context: Context): ContactDatabase?{
            if(INSTANCE == null){
                synchronized(ContactDatabase::class){
                    INSTANCE = Room.databaseBuilder(context.applicationContext,
                    ContactDatabase::class.java,"contact.db")
                        .fallbackToDestructiveMigration()
                        .allowMainThreadQueries()
                        .build()
                }
            }
            return INSTANCE
        }
    }
}

์‹ฑ๊ธ€ํ†ค์œผ๋กœ DB๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๊ณ  Room์ด ๊ธฐ๊ธฐ์˜ ๊ธฐ์กด DB๋ฅผ ํ˜„์žฌ ๋ฒ„์ „์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๊ธฐ์œ„ํ•œ ์ด์ „ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ๋‹ค๋ฉด IllegalStateException์ด ๋ฐœ์ƒํ•˜๋Š”๋ฐ ์ด์ „ ๊ฒฝ๋กœ๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์„ ๋•Œ ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์žƒ์–ด๋„ ๊ดœ์ฐฌ๋‹ค๋ฉด DB ์ƒ์„ฑ์‹œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด fallbackToDestructiveMigration()๋นŒ๋” ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์ค€๋‹ค.

Room์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ Main Thread์—์„œ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์ง€๋งŒ, allowMainThreadQueries๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ๋งŒ๋“  Instance์—์„œ๋Š” Main Thread์—์„œ๋„ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.
-> Database๋ฅผ ์ ‘๊ทผํ•˜๋Š” ์ž‘์—…์€ ๋ฌด๊ฑฐ์šด ์ž‘์—…์ด๊ณ  UI๋ฅผ ์˜ค๋žซ๋™์•ˆ ์ž ๊ธ€ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Main Thread์—์„œ ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์€ ์ข‹์ง€ ์•Š๋‹ค.


3. MVVMํŒจํ„ด์„ ์‚ฌ์šฉํ•ด ์ฃผ์†Œ๋ก ๋งŒ๋“ค๊ณ  Room์‚ฌ์šฉํ•˜์—ฌ DB ์ €์žฅ ๋ฐ ์‚ญ์ œ

Model : ContactRepository.kt

Model์€ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ž‘์—…์„ ๋งˆ์น˜๋ฉด ViewModel์—๊ฒŒ ๊ฒฐ๊ณผ๋ฅผ ์•Œ๋ ค์ค€๋‹ค.
์—ฌ๊ธฐ์„œ๋Š” LiveData๋ฅผ ํ†ตํ•ด ๊ฐ’์ด ๋ณ€ํ™”ํ•˜๋Š”์ง€ ์•Œ๋ ค์ฃผ๊ฒŒ ๋œ๋‹ค.

class ContactRepository(application: Application) {
    private val contactDatabase = ContactDatabase.getInstance(application)!!
    private val contactDao: ContactDao = contactDatabase.contactDao()
    private val contacts: LiveData<List<Contact>> = contactDao.getAll()

    fun getAll(): LiveData<List<Contact>> {
        return contacts
    }

    fun insert(contact: Contact) {
        try {
            val thread = Thread {
                contactDao.insert(contact)
            }
            thread.start()
        } catch (e: Exception) {
        }
    }

    fun delete(contact: Contact) {
        try {
            val thread = Thread {
                contactDao.delete(contact)
            }
            thread.start()
        } catch (e: Exception) {
        }
    }
}

ViewModel : ContactViewModel.kt

ViewModel์€ Model์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ UI์— ํ•„์š”ํ•œ ์ •๋ณด๋กœ ๊ฐ€๊ณตํ•˜๊ณ , View๊ฐ€ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ๋„๋ก ํ•ด๋‹น ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ "notify" ๋ฅผ ๋ณด๋‚ธ๋‹ค.
onCleared๋Š” ViewModel์„ ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฑฐ๋‚˜, ViewModel์ด ๊ด€์ฐฐํ•˜๊ณ  ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ด์•ผ ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

 class ContactViewModel(application: Application) : ViewModel(){

    private val repository = ContactRepository(application)

    fun getAll(): LiveData<List<Contact>> {
        return repository.getAll()
    }

    fun insert(contact: Contact){
        repository.insert(contact)
    }

    fun delete(contact: Contact){
        repository.delete(contact)
    }

     override fun onCleared() {
         super.onCleared()
     }
}

View : ContactActivity.kt , AddActivity.kt

AddActivity.kt : ์ฃผ์†Œ๋ก ์ถ”๊ฐ€ ํ™”๋ฉด
์ด๋ฆ„๊ณผ ๋ฒˆํ˜ธ ๊ฐ’์„ ์ž…๋ ฅํ•˜๊ณ  ์ €์žฅ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ ๊ฐ’์ด ๋น„์–ด์žˆ์ง€ ์•Š๋‹ค๋ฉด Viewmodel์˜ insert๋ฅผ ํ†ตํ•ด ๊ฐ’๋“ค์„ ๋„ฃ์–ด์ค€๋‹ค. ์ด๋ฅผ ํ†ตํ•ด View์—์„œ ViewModel์—๊ฒŒ Action์„ ์ „๋‹ฌํ•˜๊ฒŒ ๋œ ๊ฒƒ์ด๋‹ค.

class AddActivity : AppCompatActivity() {
    private val contactViewModel : ContactViewModel by viewModels{
        object : ViewModelProvider.Factory{
            override fun <T : ViewModel> create(modelClass: Class<T>): T =
                ContactViewModel(application) as T

        }
    }
    private var id : Int? = null

    private lateinit var binding: ActivityAddBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_add)

        if (intent != null && intent.hasExtra(EXTRA_CONTACT_NAME) && intent.hasExtra(EXTRA_CONTACT_NUMBER)
            && intent.hasExtra(EXTRA_CONTACT_ID)) {
            binding.addEdittextName.setText(intent.getStringExtra(EXTRA_CONTACT_NAME))
            binding.addEdittextNumber.setText(intent.getStringExtra(EXTRA_CONTACT_NUMBER))
            id = intent.getIntExtra(EXTRA_CONTACT_ID, -1)
        }

        binding.addButton.setOnClickListener {
            val name = binding.addEdittextName.text.toString()
            val number = binding.addEdittextNumber.text.toString()

            if (name.isEmpty() || number.isEmpty()) {
                Toast.makeText(this, "Please enter name and number.", Toast.LENGTH_SHORT).show()
            } else {
                val initial = name[0].uppercaseChar()
                val contact = Contact(id, name, number, initial,0)
                contactViewModel.insert(contact)
                finish()
            }
        }
    }

    companion object {
        const val EXTRA_CONTACT_NAME = "EXTRA_CONTACT_NAME"
        const val EXTRA_CONTACT_NUMBER = "EXTRA_CONTACT_NUMBER"
        const val EXTRA_CONTACT_ID = "EXTRA_CONTACT_ID"
    }
}

ContactActivity.kt : ์ฃผ์†Œ๋ก ํ™”๋ฉด
observe๋ฅผ ํ†ตํ•ด์„œ ViewModel์˜ getAll(์กฐํšŒ)์— ๋ณ€ํ™”๊ฐ€ ์ƒ๊ธฐ๋ฉด ๊ฐ์ง€ํ•ด์ค€๋‹ค.

class ContactActivity : AppCompatActivity() {

    private val contactViewModel : ContactViewModel by viewModels{
        object : ViewModelProvider.Factory{
            override fun <T : ViewModel> create(modelClass: Class<T>): T =
                ContactViewModel(application) as T
        }
    }

    private lateinit var binding: ActivityContactBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_contact)

        val aContactAdapter = ContactAdapter({ contact ->
            val intent = Intent(this, AddActivity::class.java)
            intent.putExtra(AddActivity.EXTRA_CONTACT_NAME, contact.name)
            intent.putExtra(AddActivity.EXTRA_CONTACT_NUMBER, contact.number)
            intent.putExtra(AddActivity.EXTRA_CONTACT_ID, contact.id)
            startActivity(intent)
        }, { contact ->
            deleteDialog(contact)
        })

        val layoutManger = LinearLayoutManager(this)
        binding.mainRecyclerview.adapter = aContactAdapter
        binding.mainRecyclerview.layoutManager = layoutManger
        binding.mainRecyclerview.setHasFixedSize(true)

        binding.mainButton.setOnClickListener {
            startActivity(Intent(this,AddActivity::class.java))
        }

        contactViewModel.getAll().observe(this, { contacts ->
            aContactAdapter.setContacts(contacts!!)
        })
    }

    private fun deleteDialog(contact: Contact) {
        val builder = AlertDialog.Builder(this)
        builder.setMessage("Delete selected contact?")
            .setNegativeButton("NO") { _, _ -> }
            .setPositiveButton("YES") { _, _ ->
                contactViewModel.delete(contact)
            }
        builder.show()
    }
}

ContactAdapter.kt : Recyclerview Adapter

class ContactAdapter(val contactItemClick:(Contact)->Unit, val contactItemLongClick: (Contact) -> Unit)
    :RecyclerView.Adapter<ContactAdapter.ViewHolder>(){

    private var contacts:List<Contact> = listOf()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactAdapter.ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_contact,parent,false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ContactAdapter.ViewHolder, position: Int) {
        holder.bind(contacts[position])
    }

    override fun getItemCount(): Int {
        return contacts.size
    }

    inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
        private val nameTv = itemView.findViewById<TextView>(R.id.item_tv_name)
        private val numberTv = itemView.findViewById<TextView>(R.id.item_tv_number)
        private val initialTv = itemView.findViewById<TextView>(R.id.item_tv_initial)

        fun bind(contact: Contact) {
            nameTv.text = contact.name
            numberTv.text = contact.number
            initialTv.text = contact.initial.toString()

            itemView.setOnClickListener {
                contactItemClick(contact)
            }

            itemView.setOnLongClickListener {
                contactItemLongClick(contact)
                true
            }
        }
    }

    fun setContacts(contacts: List<Contact>) {
        this.contacts = contacts
        notifyDataSetChanged()
    }

}

์ฐธ๊ณ  : https://blog.yena.io/studynote/2019/03/27/Android-MVVM-AAC-2.html
profile
Android Developer

0๊ฐœ์˜ ๋Œ“๊ธ€

๊ด€๋ จ ์ฑ„์šฉ ์ •๋ณด