Android - MVVM 패턴 적용해보기 With Room
●
오늘 알아본 것은 LIVEDATA 와 ROOM을 활용해 만든 간단한 앱이다. 위 와같이 Create, Update, Delete가 가능한 Room DB를 MVVM패턴으로 구현해보았다.!!
MVVM 패턴 이란?
Model, View, ViewModel
MVVM 패턴 또한 MVC와 마찬가지로 애플리케이션 개발에 주로 사용되는 디자인 패턴이다. 이름만 보면 알 수 있지만, MVC와 다르게 Controller가 아닌 ViewModel 계층을 가지고 있다. ViewModel 또한 Controller처럼 View와 Model의 중간 계층 역할을 하고 있다.

안드로이드 아키텍쳐 컴포넌트 ( Android Architecture Components, AAC )
안드로이드 아키텍쳐 컴포넌트는 앱 구조를 더 튼튼하고, 테스트에 용이하고, 유지 보수성이 뛰어나게 만들어 주는 라이브러리 모음이다. 아키텍쳐 컴포턴트에서는 조금 더 모듈화된 코딩을 돕기 위해 Databinding, LiveData, ViewModel 등의 유용한 라이브러리를 제공하며, 이러한 라이브러리의 모음은 MVVM 패턴의 구조의 설계에 최적화되어 있다.
따라서 MVVM에 특화 돼있는 LiveData와 ViewModel을 이용하여 앱을 제작해 볼 것이다.
1. Dependency 추가
dependencies {
def lifecycle_version = "2.5.0-alpha04"
def room_version = "2.4.2"
implementation 'androidx.activity:activity-ktx:1.1.0'
implementation 'androidx.fragment:fragment-ktx:1.2.5'
// Room components
//noinspection GradleDependency
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
2. Room생성 (Data, DAO, Database, Repository)
1. History Data Class
package com.example.room_mvvm.model
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "History")
data class History (
@PrimaryKey(autoGenerate = true)
var id: Int,
var songName: String,
var singerName: String
)
- PrimaryKey 인 ID 값은 autoGenerate를 통해 자동으로 다음 번호가 생성이 되는 ID값이 만들어진다.
2. HistoryDAO
package com.example.room_mvvm.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.example.room_mvvm.model.History
@Dao
interface HistoryDao{
@Delete
suspend fun delete(taskEntry: History)
@Update
suspend fun update(taskEntry: History)
@Insert
suspend fun insert(taskEntry: History)
@Query("DELETE FROM History")
suspend fun deleteAll()
@Query("SELECT * FROM History")
fun getAll(): LiveData<List<History>>
}
- suspend를 사용한 이유는 비동기식으로 일시 중단 가능한 코루틴을 만들기 위함 간단한 CURD DAO 구현
- 여기서 <List<History>>를 LiveData로 담아주어 변경 즉시 UI에 적용 될 수 있게 한다.
3. AppDataBase
@Database(entities = [History::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun historyDao(): HistoryDao
companion object{
private var INSTANCE : AppDatabase ?= null
fun getDataBase(context: Context) : AppDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"task_database"
).fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
- exportSchema : False -> Schema를 외부에 내보낼 수 없다.
- 안드로이드 개발자 문서에서 데이터베이스 개체를 단일 프로세스앱에서 실행 될 경우 싱글톤 디자인 패턴을 따라야 한다고 합니다.
싱글톤 패턴은 어떤 클래스를 최초 단 한번만 생성하여 메모리에 할당하고 그 메모리를 참조해서 사용하는 디자인 패턴을 말합니다.
- abstract Class는 아직 미완성된 클래스이다. 따라서 함수 또한 미완성 된 경우가 많아 함수 또한 Abstract modifier가 주로 붙는다. abstact는 그자체로 미완성 됐으므로 객체화가 불가능하다.
4. Repository
class SetRepository(val historyDao : HistoryDao) {
suspend fun insert(history: History) = historyDao.insert(history)
suspend fun update(history: History) = historyDao.update(history)
suspend fun delete(history: History) = historyDao.delete(history)
fun getAlltasks() : LiveData<List<History>> = historyDao.getAll()
}
- CRUD 구현 완료한 것들을 각각 초기화 해줍니다 ViewModel에서 DB에 접근을 요청할 때 수행할 함수를 만들어둡니다.
3. ViewModel 구현
본격 적인 MVVM 패턴 구현
class HistoryViewModel(application: Application) : AndroidViewModel(application){
private val historyDao = AppDatabase.getDataBase(application).historyDao()
private val repository : SetRepository
var getAllTasks: LiveData<List<History>>
init {
repository = SetRepository(historyDao)
getAllTasks = repository.getAlltasks()
}
fun insert(history: History) {
viewModelScope.launch(Dispatchers.IO){
repository.insert(history)
}
}
fun delete(history: History){
viewModelScope.launch(Dispatchers.IO){
repository.delete(history)
}
}
fun update(history: History){
viewModelScope.launch(Dispatchers.IO){
repository.update(history)
}
}
}
- historyDao : 싱글톤으로 구현 한 getDataBase()를 불러와 준다.
- Application은. AndroidViewModel은 Application을 상속받기 때문에 메모리 leak이 발생할 수 있다.
- Context 작업이 필요할 경우 AndroidViewModel을 써준다.
- viewModelScope를 통해 코루틴 형식의 동작 방식을 따라 준다.
4. RecyclerViewAdpater 구현
RecyclerView를 통해 카드뷰 형식의 개체가 삭제 수정 삽입 되는게 보여질 것이기에 구현 하였다.
class recyclerViewAdapter(listener: OnItemClick) : RecyclerView.Adapter<recyclerViewAdapter.ViewHolder>() {
private val mCallback = listener
private val items = ArrayList<History>()
inner class ViewHolder(private val binding: ListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(songModel: History) {
binding.title.text = songModel.songName
binding.description.text = songModel.singerName
binding.deleteButton.setOnClickListener {
mCallback.deleteTodo(songModel)
}
binding.updateButton.setOnClickListener {
mCallback.updateTodo(songModel)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemBinding.inflate(layoutInflater)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
fun setList(history: List<History>) {
items.clear()
items.addAll(history)
}
override fun getItemCount(): Int {
return items.size
}
}
- setList를 통해 recyclerview itemList를 지속적으로 변경 될때마다 적용해준다.
- listener인 OnItemClick interface 형식으로 구현하고 Callback하여 구현 해주었다.
interface OnItemClick {
fun deleteTodo(history: History)
fun updateTodo(history: History)
}
5. MainActivity 구현
- ViewBinding 사용
- observe를 통한 LiveData 감시 / observe : 감시하다 라는 의미로 실시간으로 변경되는 LiveData가 UI에 나올 수 있도록 뿌려줌
class MainActivity : AppCompatActivity(), OnItemClick {
private lateinit var adapter: recyclerViewAdapter
private lateinit var binding: ActivityMainBinding
private val historyViewModel: HistoryViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initRecyclerView()
historyViewModel.getAllTasks.observe(this, Observer {
adapter.setList(it)
adapter.notifyDataSetChanged()
})
binding.addButton.setOnClickListener {
val songTitle = binding.songTitleName.text.toString()
val singer = binding.singerName.text.toString()
val history = History(
0,
songTitle,
singer
)
historyViewModel.insert(history)
binding.singerName.setText("")
binding.songTitleName.setText("")
}
}
private fun initRecyclerView() {
binding.recyclerView.layoutManager = LinearLayoutManager(this)
adapter = recyclerViewAdapter(this)
binding.recyclerView.adapter = adapter
}
override fun deleteTodo(history: History) {
historyViewModel.delete(history)
}
override fun updateTodo(history: History) {
binding.cardView.isVisible = true
binding.scrollView.isVisible = false
binding.updateSong.setText(history.songName)
binding.updatePassword.setText(history.singerName)
binding.updateResultButton.setOnClickListener {
val songTitle = binding.updateSong.text.toString()
val singer = binding.updatePassword.text.toString()
val newHistory = History(
history.id,
songTitle,
singer
)
historyViewModel.update(newHistory)
binding.updateSong.setText("")
binding.updatePassword.setText("")
binding.cardView.isVisible = false
binding.scrollView.isVisible = true
}
}
}
- 아까 구현해주었던 OnitemClick Interface를 상속받아 override하여 재정의 해준다. Adapter를 통해 Callback 받았으므로 이렇게 사용해주면 된다.
< 느낀점 >
- MVVM 패턴에 더욱더 익숙해지려고 해야 겠다. 추가적으로 코루틴 스코프를 더욱더 잘 이해해서 조금더 나은 효율을 가진 비동기식 앱을 MVVM 패턴과 함께 구현하도록 해야겠다 !