안드로이드/정리(Android)

Android - MVVM 패턴 적용해보기 With Room

김염인 2022. 4. 6. 19:50

 

오늘 알아본 것은 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 패턴과 함께 구현하도록 해야겠다 !