MVVM ๋ ๋ฒ์งธ ์๊ฐ์ด๋ค. ๊ฐ์์ค๋ฝ๊ฒ ์ฐพ์์จ ์ด์ ๋ ์กธํ๋ฅผ ์งํํ๋ค๊ฐ RecyclerView
๋ฅผ Room
์ ์ฌ์ฉํด์ MVVM ํจํด
์ผ๋ก ๊ตฌํํ๋๋ฐ ์ ์์ ์ผ๋ก ์๋ํ์ง ์์์ RecyclerView + Room + MVVM
์ ์ ๋ฆฌํ๊ณ ์ฝ๋๋ฅผ ๋ค์ ๋ณด๋ ค๊ณ ํ๋ค.
์ฐ์ MVVM
์ ๊ตฌํํ๊ธฐ ์ํด์๋ ์์ ๊ทธ๋ฆผ์ ์ดํดํ๊ณ ๋์ด๊ฐ๋ ๊ฒ์ด ์ข๋ค. ๊ทธ๋ฆผ์์ ์ฃผ์๊น๊ฒ ๋ด์ผํ ๊ฒ์ ํ์ดํ์ ๋ฐฉํฅ
์ด๋ค. ๋ชจ๋ ํ์ดํ๊ฐ ๋จ๋ฐฉํฅ์ผ๋ก ์ฐ๊ฒฐ ๋์ด์๊ณ ์์ ์์๋ ํ์ ์์๋ฅผ ์ฐธ์กฐํ๋ค. ์ฐธ์กฐํ ๋ ๋ฐ๋ก ์๋์ ์๋ ์์๋ง ์ฐธ์กฐ ๊ฐ๋ฅํ๋ค. ๊ฐ๋ น Activity
์ Fragment
๊ฐ ๋ฐ๋ก ์๋ ์๋ ViewModel
์ด ์๋ Respository
๋ฅผ ์ฐธ์กฐํ๋ฉด ์๋๋ค. ์ด ๊ฐ๋
์ ๋จธ๋ฆฌ์ ์๊ฒจ๋๊ณ ๊ฐ๋จํ ์ดํ๋ฆฌ์ผ์ด์
์ ๊ตฌํํด๋ณด์.
์ฐ์ ์ดํ๋ฆฌ์ผ์ด์ ์ ์ ์ฒด์ ์ธ ๊ตฌ์กฐ์ ๊ตฌํ ๋ชจ์ต์ ์๋์ ๊ฐ๋ค.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/editTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:ellipsize="end"
android:hint="์ํ ์ ๋ณด๋ฅผ ์
๋ ฅํ์ธ์."
android:inputType="text"
android:lines="1"
android:maxLines="1"
app:layout_constraintBottom_toTopOf="@+id/btn_add"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_add"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:text="์ถ๊ฐ"
app:layout_constraintBottom_toTopOf="@+id/recyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editTextView" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_add" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
์ฐ์ ๋ ์ด์์
๋ถํฐ ๊ตฌํํ๋ค. ์๋จ์ EditTextView
๋ฐ๋ก ์๋์ ๋ฑ๋ก ๋ฒํผ
ํ๋จ์๋ ๋ฑ๋ก๋ ์ํ์ ๋ณด์ฌ์ฃผ๋ RecyclerView
๋ฅผ ๋ฐฐ์นํ๋ค.
item_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="12dp">
<androidx.cardview.widget.CardView
android:id="@+id/itemCardView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="#C5E1A5"
app:cardCornerRadius="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/itemTextView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="12dp"
android:ellipsize="end"
android:gravity="center"
android:lines="1"
android:maxLines="1"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_remove"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="๋ฏธ๋ฆฌ๋ณด๊ธฐ์ฉ ํ
์คํธ์
๋๋ค." />
<ImageButton
android:id="@+id/btn_remove"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_margin="12dp"
android:background="@drawable/ic_baseline_delete_forever_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/itemTextView"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
RecyclerView
์์ ๋ณด์ฌ์ค ์์ดํ
์ ๋ํ ๋ ์ด์์
์ ๊ตฌํํ๋ค.
์ฝ๋๋ฅผ ์ ์ฉํ๋ฉด ์๋์ ๊ฐ์ UI๋ฅผ ๊ฐ์ง๋ค.
Product
package com.example.mvvm_recyclerview_room.database
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Product(
@PrimaryKey(autoGenerate = true) val id: Int?,
val title: String
)
์ผ๋ฐ์ ์ธ DataClass
์ Entity
๋ผ๋ ์ด๋
ธํ
์ด์
์ ์ถ๊ฐํ๋ค. ๋ํ PrimaryKey
๋ก ์ฌ์ฉํ๊ธฐ ์ํด id
๋ฅผ ์ง์ ํด์คฌ๊ณ ์ด ๋ถ๋ถ์ ์ฌ์ฉ์๊ฐ ์
๋ ฅํ๋ ๊ฒ์ด ์๋๋ผ ์๋์ผ๋ก ์์คํ
์์ ์
๋ ฅ๋ ์ ์๋๋ก autoGenerate = true
์์ฑ์ ๋ถ์ฌํ๋ค.
ProductDao
package com.example.mvvm_recyclerview_room.database
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface ProductDao {
@Insert
suspend fun insert(product: Product)
@Update
suspend fun update(product: Product)
@Delete
suspend fun delete(product: Product)
@Query("SELECT * FROM product")
fun getAll(): LiveData<List<Product>>
}
suspend
๋ผ๋ ์ฒ์ ๋ณด๋ ํค์๋๊ฐ ๋ฑ์ฅํ๋ค. suspend
๋ ์ผ์ ์ค๋จ์ ์ฌ์ฉํ๋ ํจ์๋ก ํด๋น ๋ฉ์๋๊ฐ ํธ์ถ๋ ๋ ์ผ์ ์ค๋จ๋๋ค. ๋ฐ๋ผ์ ์ฝ๋ฃจํด ๋ธ๋ญ ๋ด๋ถ
์ด๋ ๋ค๋ฅธ ์ผ์ ์ค๋จ ํจ์ ๋ด๋ถ
์์ ์คํ๋์ด์ ธ์ผ๋ง ํ๋ค.
ProductDatabase
package com.example.mvvm_recyclerview_room.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Product::class], version = 1)
abstract class ProductDatabase : RoomDatabase() {
abstract fun productDao(): ProductDao
companion object {
private var instance: ProductDatabase? = null
@Synchronized
fun getInstance(context: Context): ProductDatabase? {
if (instance == null) {
synchronized(ProductDatabase::class) {
instance = Room.databaseBuilder(
context.applicationContext,
ProductDatabase::class.java,
"product-database"
).build()
}
}
return instance
}
}
}
์์์ ๊ตฌํํ Entity
๋ฅผ ๊ฐ์ง๊ณ ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค
๋ฅผ ๊ตฌํํ๋ค. ์ฑ๊ธํค
ํจํด์ผ๋ก ์์ฑํ๋ค.
Repository && RepositoryImpl
package com.example.mvvm_recyclerview_room.repository
import androidx.lifecycle.LiveData
import com.example.mvvm_recyclerview_room.database.Product
interface ProductRepository {
suspend fun insert(product: Product)
suspend fun update(product: Product)
suspend fun delete(product: Product)
fun getAll(): LiveData<List<Product>>
}
package com.example.mvvm_recyclerview_room.repository
import android.app.Application
import androidx.lifecycle.LiveData
import com.example.mvvm_recyclerview_room.database.Product
import com.example.mvvm_recyclerview_room.database.ProductDatabase
class ProductRepositoryImpl(application: Application) : ProductRepository {
private val productDao by lazy {
ProductDatabase.getInstance(application)!!.productDao()
}
override suspend fun insert(product: Product) {
productDao.insert(product)
}
override suspend fun update(product: Product) {
productDao.update(product)
}
override suspend fun delete(product: Product) {
productDao.delete(product)
}
override fun getAll(): LiveData<List<Product>> {
return productDao.getAll()
}
}
Activity - ViewModel - Respository - Data
๋ผ๋ ํฐ ํ์์ ๋ดค์ ๋ Interface
์ญํ ์ ์ํํ๋ค. Respository
๋ก ์ธํด์ ViewModel
์ด ์ง์ ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ ํ์๊ฐ ์์ด์ง๋ค. ์์์ ๊ตฌํํ Dao
๊ฐ suspend
์ด๋ฏ๋ก ์ด๋ฅผ ํธ์ถํ๋ ๋ฉ์๋ ์ญ์ suspend
๋ก ๊ตฌํํ๋ค.
ProductViewModel
package com.example.mvvm_recyclerview_room.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import com.example.mvvm_recyclerview_room.database.Product
import com.example.mvvm_recyclerview_room.repository.ProductRepositoryImpl
class ProductViewModel(application: Application) : AndroidViewModel(application) {
private val repository = ProductRepositoryImpl(application)
private val products = repository.getAll()
suspend fun insert(product: Product) {
repository.insert(product)
}
suspend fun update(product: Product) {
repository.update(product)
}
suspend fun delete(product: Product) {
repository.delete(product)
}
fun getAll(): LiveData<List<Product>> {
return products
}
}
repository
๋ฅผ ํตํด ๋ฐ์ดํฐ์ ๋ํ CRUD
์ ์งํํ๋ค. ์ญ์ repository
์ suspend
๋ฅผ ํธ์ถํ๋ฏ๋ก suspend
๋ก ๊ตฌํํ๋ค.
BaseActivity
package com.example.mvvm_recyclerview_room.base
import android.os.Bundle
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
abstract class BaseActivity<T : ViewDataBinding>(@LayoutRes private val layoutResId: Int) :
AppCompatActivity() {
private var _binding: T? = null
val binding get() = _binding ?: error("Not Initialized")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = DataBindingUtil.setContentView(this@BaseActivity, layoutResId)
binding.lifecycleOwner = this@BaseActivity
}
}
์์
ํ๊ธฐ ์์ BaseActivity
๋ฅผ ๊ตฌํํ๋ค. ๋ฌผ๋ก ๊ตณ์ด? ๊ตฌํํ ํ์ ์๋ ํด๋์ค์ด๋ค. ๊ทธ๋ฅ ๋ฐ๋ก AppCompatActivity
๋ฅผ ์์๋ฐ์์ ๊ตฌํํด๋ ๋๋ค. ํ์ง๋ง ๋ค๋ฅธ ํ๋ก์ ํธ์์ Activity
๋ Fragment
์์ ๊ฒน์น๋ ์ฝ๋๋ค์ Base
๋ก ๋ถ๋ฆฌํ๊ณ ์์
ํ๋๊น ์ค๋ณต๋๋ ์ฝ๋๊ฐ ํ์คํ ์ค์ด์ ํธํ๋ค. ์ง๊ธ ์ด ํ๋ก์ ํธ๋ Single Activity
์ด์ง๋ง ์์ ์ตํ๊ณ ์ BaseActivity
๋ก ๋ถ๋ฆฌํ๋ค.
BaseActivity
์ ๋ฌด์์ ๊ตฌํํ ์ง๋ ๊ฐ๋ฐ์์ ์
๋ง์ธ ๊ฒ ๊ฐ๋ค. ๊ทธ๋๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์์ด์ผํ๋ ๊ฒ์ ์ฐ๊ฒฐํด ์ค DataBinding
๊ณผ onCreate
์ด๋ค. DataBinding
์ญ์ ์ง์ ์ ์ผ๋ก ์ฐ๊ฒฐํด์ฃผ์ง ์๊ณ private
๋ก ๊ตฌํํ๊ณ ์ธ๋ถ์์ ์ฐธ์กฐํ ๋๋ ๋ณ๋์ ๋ณ์๋ก ์ ๊ทผํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ค.
MainActivity
package com.example.mvvm_recyclerview_room
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.mvvm_recyclerview_room.adapter.ProductAdapter
import com.example.mvvm_recyclerview_room.base.BaseActivity
import com.example.mvvm_recyclerview_room.database.Product
import com.example.mvvm_recyclerview_room.databinding.ActivityMainBinding
import com.example.mvvm_recyclerview_room.viewmodel.ProductViewModel
import kotlinx.coroutines.*
class MainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) {
private val viewModel by lazy {
ViewModelProvider(this)[ProductViewModel::class.java]
}
private val adapter by lazy {
ProductAdapter(itemClickListener = { product ->
deleteProduct(product)
})
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initViews()
bindViews()
subscribeObserver()
}
private fun initViews() = with(binding) {
recyclerView.layoutManager = LinearLayoutManager(this@MainActivity)
recyclerView.adapter = adapter
}
private fun bindViews() = with(binding) {
btnAdd.setOnClickListener {
insertProduct(editTextView.text.toString())
}
}
private fun subscribeObserver() {
viewModel.getAll().observe(this) { adapter.submitList(it) }
}
private fun insertProduct(title: String) {
CoroutineScope(Dispatchers.IO).launch {
viewModel.insert(Product(id = null, title = title))
}
}
private fun deleteProduct(product: Product) {
CoroutineScope(Dispatchers.IO).launch {
viewModel.delete(product)
}
}
}
abstract
๋ก ๊ตฌํํ BaseActivity
๋ฅผ ์์๋ฐ๊ณ ViewModel
์ ViewModelProvider
๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํํ๋ค. (ViewModelProviders๋ Deprecated ๋์๋ค.) RecyclerView
์ ์ฐ๊ฒฐํด ์ค Adapter
๋ ๊ตฌํํ๊ณ LayoutManager
๋ฅผ ์ง์ ํด์ค๋ค.
subcribeObserver
์ insertProduct
deleteProduct
๊ฐ ๋์ ๋๋ค. subcribeObserver
๋ viewModel.getAll()
์ ๊ด์ฐฐํ๊ณ ์๋ค๊ฐ ๋ณํ๊ฐ ์ผ์ด๋๋ฉด RecyclerView
์ ํ์ํ ์์ดํ
(๋ฆฌ์คํธ)๋ฅผ ๊ฐฑ์ ํด์ฃผ๋ ๋ฉ์๋์ธ adapter.submistList
๋ฅผ ์คํํ๋ค.
์ค๋ช
์ ์ํด ์ ๊น ProductAdapter
๋ก ์ด๋ํด๋ณด์.
ProductAdapter
package com.example.mvvm_recyclerview_room.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.mvvm_recyclerview_room.database.Product
import com.example.mvvm_recyclerview_room.databinding.ItemMainBinding
class ProductAdapter(private val itemClickListener: (Product) -> Unit) :
RecyclerView.Adapter<ProductAdapter.ViewHolder>() {
private var products: List<Product>? = null
inner class ViewHolder(private val binding: ItemMainBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bindViews(product: Product) {
binding.itemTextView.text = product.title
binding.btnRemove.setOnClickListener {
itemClickListener(product)A
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemMainBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
products?.let {
holder.bindViews(it[position])
}
}
override fun getItemCount(): Int {
return products?.size ?: 0
}
fun submitList(items: List<Product>) {
products = items
notifyDataSetChanged()
}
}
๋๋ถ๋ถ์ ์ฝ๋๋ ๊ธฐ์กด RecyclerView
์ Adapter
์ ๋งค์ฐ ์ ์ฌํ๋ค. itemClickListener
๋ฅผ ํตํด Product
๋ฅผ ์ ๋ฌํ๊ณ ์ด๋ฅผ MainActivity
์์ ์ฌ์ฉํ ์ ์๊ฒ๋ ํ๋ค. ์์์ ์ค๋ช
ํ submitList
๋ ์ ๋ฌ ๋ฐ์ ์์ดํ
(๋ฆฌ์คํธ)๋ก ๊ฐฑ์ ํ๋ค. notifyDataSetChanged
๋ฅผ ์ฌ์ฉํ์ฌ RecyclerView
์ ์๋ฆฐ๋ค.
MainActivity
private fun insertProduct(title: String) {
CoroutineScope(Dispatchers.IO).launch {
viewModel.insert(Product(id = null, title = title))
}
}
private fun deleteProduct(product: Product) {
CoroutineScope(Dispatchers.IO).launch {
viewModel.delete(product)
}
}
๋ค์ MainActivity
๋ก ๋์ด์๋ค. ๋ ๊ฐ์ ๋ฉ์๋๋ Room
์ผ๋ก ๊ตฌํํ ๋ด์ฅ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํญ๋ชฉ์ ์ถ๊ฐํ๊ณ ์ญ์ ํ๋ ๊ธฐ๋ฅ์ด๋ค. insert
์ delete
์์๋ ์ฝ๋ฃจํด
์ด๋ผ๋ ์๋ก์ด ๊ฐ๋
์ด ๋ฑ์ฅํ๋๋ฐ ๋คํธ์ํน ์์
์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ๊ฒฝ์ฐ ๋ฉ์ธ ์ฐ๋ ๋
์์ ์์
ํ๋ฉด ์๋๋ค. ์ด๋ฐ ๊ฐ๋จํ ํ๋ก์ ํธ๋ ๊ด์ฐฎ์ง๋ง ๋ฌด๊ฑฐ์ด ์์
์ ๋ฉ์ธ ์ฐ๋ ๋์์ ์์
ํ๊ฒ ๋๋ฉด ANR(Application Not Responding)
์ด ๋ฐ์ํ๋ฉด์ ์ฑ์ด ๊ฐ์ ์ข
๋ฃ
๋๊ณ ์ฌ์ฉ์์๊ฒ ๋ถํธํจ์ ์ค ๊ฒ์ด๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด UI ์์
์ ์ ์ธํ๊ณ ๋ค๋ฅธ ์ฐ๋ ๋์์ ์งํํ๋๋ฐ ์ด๋ฅผ ๋์์ฃผ๋ ๊ฒ์ด ์ฝ๋ฃจํด
์ด๋ค. CoroutineScope
์ผ๋ก ์คํ๋ ๋ฒ์๋ฅผ ์ง์ ํ๊ณ Dispatcher
๋ก ์ด๋ค ์ฐ๋ ๋
์์ ์์
ํ ์ง๋ฅผ ์ง์ ํ๋ค.
๊ตฌํ ๊ฒฐ๊ณผ
๋๋ ์
์ฌ์ค ์ง๊ธ ์ด๋ ๊ฒ ๋ณด๋ ์๋ชป ๊ตฌํ๋์๋ค๊ณ ์๊ฐํ๋ค. MainActivity
์์๋ Data
์ ๋ํ ์ ๊ทผ์ ์ง์ํด์ผํ๋๋ฐ ์ง๊ธ ๊ตฌํํ ์ฝ๋๋ฅผ ๋ณด๋ฉด Product
๊ฐ์ฒด๋ฅผ ์์ฑํด์ ์ ๋ฌํ๊ณ ์๋ค. ๋ค์ ๊ณ ์ณ๋ด์ผ๊ฒ ๋ค.
'๐ป ๊ฐ๋ฐ > Android' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Android] ์๋ฆผ ํด๋ฆญ์ Activity, Fragment๋ก ์ด๋ (0) | 2022.06.08 |
---|---|
[Android] ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์์ผ ํต์ ์ผ๋ก ์ด๋ฒคํธ ์์ ํ ์๋ฆผ (0) | 2022.06.08 |
[Android] MVVM ํจํด ์ ์ฉ๊ธฐ - 1 (0) | 2022.04.11 |
[Android] ์นด๋ฉ๋ผ ๋๋ ๊ฐค๋ฌ๋ฆฌ์์ ์ด๋ฏธ์ง ๊ฐ์ ธ์ค๊ธฐ (0) | 2022.02.06 |
[Android] ๋ค๊ตญ์ด ์ง์ (0) | 2022.01.13 |