본문 바로가기
오늘은 뭘 배울까?/Android

RecyclerView + MVVM + Room을 연습해보자!

by Kim Juhwan 2021. 3. 11.

연습한 내용을 기록하는 거라

다른 게시글에 비해 다소 설명 불친절할 수 있습니다(?)

강의글이 아니에요!

 

 

 

0. 프롤로그

항상 감사합니다. 도와주시는 분들 너무 착함 ㅠㅠ

 

처음에 나는 MVVM이란 구조가 명확히 있고 같이 사용하면 좋은 라이브 데이터, 데이터 바인딩 이런 거를 같이 사용해야 진정한 Clean Architecture다!라고 생각하고 고민을 했었다.

하지만 예제들을 보면 어떤 사람은 이걸 쓰고 어떤 사람은 이거 말고 저걸 쓰고 또 구현하는 방식도 제각각이고 등등..

너무 헷갈려서 오픈 채팅방에 SOS 요청을 했더니 위와 같이 답변을 해주셨다. (내 생각을 정확히 꿰뚫으심...)

 

그래서 처음부터 너무 틀에 구애받지 말고 처음부터 너무 완벽하게 하려고 하지 말고

이해한 부분부터 차근차근 구조를 완성시켜보고 나중에 새로운 걸 추가시키더라도 일단은 만들어보기로 했다.

 

 

디자인 따위는 지나가던 뽀삐에게 주고 왔으니 기능만 보자.

 

아무튼 그래서 오늘 만들 것은 이거다.

Room을 이용해 데이터를 내부 DB에 저장하고 View Model, Live Data 그리고 RecyclerView까지 쓰는 짬뽕세엣-트

 

1. Room

우선 이거부터...

내가 MVVM이고 자시고 뭐고 모르고 액티비티에 코드를 다 때려 넣었을 때는 나는 폴더를 프래그먼트/액티비티로 구분해서 넣곤 했었다. 이번에 다른 분들의 코드를 쭉 둘러보니까 database / repository 이런 식으로 기능별로 나눌 필요가 있다는 것을 알게 됐다. 암튼 그래서 데이터베이스 코드부터..

 

@Entity
data class Todo (
    var content: String
){
    @PrimaryKey(autoGenerate = true) var id: Int = 0
}
@Dao
interface TodoDao {
    @Query("SELECT * FROM Todo")
    fun getAll(): LiveData<List<Todo>>

    @Insert
    fun insert(todo: Todo)

    @Update
    fun update(todo: Todo)

    @Delete
    fun delete(todo: Todo)
}
@Database(entities = [Todo::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun todoDao(): TodoDao

    companion object {
        private var instance: AppDatabase? = null

        @Synchronized
        fun getInstance(context: Context): AppDatabase? {
            if (instance == null) {
                synchronized(AppDatabase::class){
                    instance = Room.databaseBuilder(
                            context.applicationContext,
                            AppDatabase::class.java,
                            "database-name"
                    ).build()
                }
            }
            return instance
        }
    }
}

 

Room에 대한 설명은 여기에서 볼 수 있다.

 

2. Repository

class TodoRepository(application: Application) {
    private val todoDao: TodoDao
    private val todoList: LiveData<List<Todo>>

    init {
        var db = AppDatabase.getInstance(application)
        todoDao = db!!.todoDao()
        todoList = db.todoDao().getAll()
    }

    fun insert(todo: Todo) {
        todoDao.insert(todo)
    }

    fun delete(todo: Todo){
        todoDao.delete(todo)
    }

    fun getAll(): LiveData<List<Todo>> {
        return todoDao.getAll()
    }
}

원래는 Repository 없이 뷰모델에서 다 처리하려고 했는데 공식문서에 지키지는 않아도 되지만 권장사항이다 정도로 표현하고 있는 것 같아서 만들었다. 일단 메모 추가/삭제/읽기가 되어야 하니까 이렇게 메서드를 3개 추가해주었다.

 

LiveData는 ViewModel이 가지고 있는 데이터를 수정하는 게 아니라 DB에 있는 데이터를 수정해야 하기 때문에 MutableLiveData를 사용하지 않았다.

 

3. ViewModel

class TodoViewModel(application: Application) : AndroidViewModel(application) {
    private val repository = TodoRepository(application)
    private val items = repository.getAll()

    fun insert(todo: Todo) {
        repository.insert(todo)
    }

    fun delete(todo: Todo){
        repository.delete(todo)
    }

    fun getAll(): LiveData<List<Todo>> {
        return items
    }
}

repository에서 application context가 필요하기 때문에 ViewModel이 아닌 AndroidViewModel을 상속받았다.

repository 덕분에 뷰모델이 딱 지 할 일만 하게 됐다. 회사에서 이러면 이쁨 못 받을 텐데...

 

ViewModel에 대한 설명은 여기에서 볼 수 있다.
블로그 주인장의 따뜻하고 배려 넘치는 게시물이 아니라 굳이 딱딱한 공식문서를 보고 싶다면 여기에서 볼 수 있다. ^^

 

4. MainActivity

class MainActivity : AppCompatActivity(), OnItemClick {

    private lateinit var binding: ActivityMainBinding
    private val model: TodoViewModel by viewModels()
    private lateinit var adapter: TodoAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initRecyclerView()

        model.getAll().observe(this, Observer{
            adapter.setList(it)
            adapter.notifyDataSetChanged()
        })

        binding.btnAdd.setOnClickListener {
            lifecycleScope.launch(Dispatchers.IO){
                model.insert(Todo(binding.editText.text.toString()))
            }
        }
    }

    private fun initRecyclerView(){
        binding.recyclerViewTodo.layoutManager = LinearLayoutManager(this)
        adapter = TodoAdapter(this)
        binding.recyclerViewTodo.adapter = adapter
    }

    override fun deleteTodo(todo: Todo) {
        lifecycleScope.launch(Dispatchers.IO){
            model.delete(todo)
        }
    }
}

메인 액티비티에서는 findViewById 대신 뷰 바인딩을 사용했다.

데이터 바인딩을 사용해도 되지만 난 아직 익숙하지가 않다 😔

(사실 편한 지도 잘 모르겠다. xml에 코드 있는 거 복잡하고 싫은데..)

 

observer를 붙여서 데이터가 변경되면 어댑터에게 넘기고 새로고침을 하였다.

추가 버튼을 누르면 insert를 하는데 이때 메인 스레드에서 실행하면 안 되므로 코루틴을 사용했다.

 

리사이클러뷰 내의 삭제 버튼을 구현하기 위해서 콜백 함수를 사용했다.

initRecyclerView에서 this로 listener를 넘겨준 이유가 이것 때문이다.

삭제 메서드 역시 코루틴을 사용했다.

 

뷰바인딩에 대한 설명은 여기에서 볼 수 있다.
코루틴에 대한 설명은 여기에서 볼 수 있다.

 

5. TodoAdapter

class TodoAdapter(listener: OnItemClick) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {

    private val mCallback = listener
    private val items = ArrayList<Todo>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : TodoViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = TodoItemBinding.inflate(layoutInflater)
        return TodoViewHolder(binding)
    }

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

    override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
        holder.bind(items[position])
    }

    fun setList(todo: List<Todo>) {
        items.clear()
        items.addAll(todo)
    }

    inner class TodoViewHolder(private val binding: TodoItemBinding):RecyclerView.ViewHolder(binding.root){

        fun bind(item: Todo){
            binding.tvTodo.text = item.content
            binding.ivIcon.setOnClickListener {
                mCallback.deleteTodo(item)
            }
        }
    }
}

원래는 데이터 바인딩을 같이 사용하려고 했는데... 🤢🤮 일단 뷰 바인딩을 쓰기로 했다.

Clean Architecture의 기본 개념이 UI 기반 클래스를 가볍게 하자! 이니까 위 코드에서 작성한 setList에서의 작업은 액티비티에서 하고 어댑터한테 넘겨주는 것보다 어댑터 안에서 처리하는 게 맞는 것 같다.

 

데이터 바인딩에 대한 설명은 여기에서 볼 수 있다.

 

6. OnItemClick

interface OnItemClick {
    fun deleteTodo(todo: Todo)
}

 

 

 

 

뭐 아무튼 이렇게 끝!

얼른 배워서 출시했던 앱도 구조를 바꿔야겠다.


💡 느낀 점

  • 확실히 액티비티는 가벼워진 것 같다. 역할도 나눠지니 보기도 편한 것 같고...
  • 꼭 데이터 바인딩 쓴다고 좋은 건 아닌 듯! 각자 상황이 있고 각자 편한 게 있으니까
  • 하지만 취업시켜주신다면 입 다물고 쓰겠습니다요 🤐 
  • Live Data에 대해 모를 때는 일일이 업데이트하고 했는데 알아서 해주니까 너무 편하다.

📘 참고한 자료


반응형

댓글