๐MVVMํจํด๊ณผ Room ์ฌ์ฉํ์ฌ ์ฃผ์๋ก ๋ง๋ค๊ธฐ (Kotlin)
// 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"
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์์ ์ ๊ทผํ๋ ๊ฒ์ ์ข์ง ์๋ค.
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()
}
}