본문 바로가기

안드로이드

[안드로이드] 예제:MVVM+AAC를 이용한 RecyclerView 4

구현 과정

1. 프로젝트 구성

2. Room 영속성 라이브러리 관련 클래스 구현

3. Repository 구현

4. ViewModel 구현

5. RecyclerViewAdapter 구현

6. MainActivity 구현

7. AddActivity 구현


MainActivity.kt : UI Controller(View)

먼저 xml 파일을 살펴보자.

 

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">

    <data>
        <variable
            name="activity"
            type="com.example.sampleapp.MainActivity" />

        <variable
            name="viewModel"
            type="com.example.sampleapp.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/textview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="Hello MVVM!"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginTop="16dp"
            app:layout_constraintBottom_toTopOf="@+id/main_button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textview"
            tools:listitem="@layout/item_todo" />

        <Button
            android:id="@+id/main_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:text="Add"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent" />


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

위를 보면 data의 변수로 MainActivity, ViewModel이 존재한다. 이는 관찰을 위한 변수이다. 그 다음 MainActivity를 살펴보자.

 

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    val ViewModel: MainViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main)

        binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.viewModel = ViewModel
        setRecyclerView()

        binding.mainButton.setOnClickListener {
            val intent = Intent(this, AddActivity::class.java)
            startActivity(intent)
        }
    }

    private fun deleteDialog(todo: Todo) {
        val builder = AlertDialog.Builder(this)
        builder.setMessage("Delete selected contact?")
            .setNegativeButton("취소") { _, _ -> }
            .setPositiveButton("편집") { _, _ ->
                val intent = Intent(this, AddActivity::class.java)
                intent.putExtra(AddActivity.EXTRA_TODO_TITLE, todo.title)
                intent.putExtra(AddActivity.EXTRA_TODO_DESC, todo.description)
                intent.putExtra(AddActivity.EXTRA_TODO_ID, todo.id)
                startActivity(intent)
            }.setNeutralButton("삭제"){_, _ ->
                lifecycleScope.launch(Dispatchers.IO){ViewModel.delete(todo)}
            }
        builder.show()
    }

    private fun setRecyclerView(){
        // Set contactItemClick & contactItemLongClick lambda
        val adapter = TodoAdapter ({ todo -> deleteDialog(todo)},{ todo -> deleteDialog(todo)})

        binding.recyclerView.adapter=adapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        binding.recyclerView.setHasFixedSize(true)

        ViewModel.getAll().observe(this, Observer { todos ->
            adapter.setTodos(todos!!)
        })
    }
}

아이템의 변화를 관찰하기 위해 바인딩 생성 및 뷰모델을 생성하였다. 그리고 setRecyclerView를 통해 어탑터를 생성하여 각종 세팅을 해주는데 중요한 점은 ViewModel.getAll().observe... 부분이다. 이 부분을 통해 실질적인 관찰이 이루어지고 변화를 감지한다. 변화가 감지되면 변경된 todos 리스트를 어답터에 전달하여 초기화한다. (참고로 binding.lifecyclerOwner=this 부분은 굳이 삽입하지 않아도 된다.) 나머지 코드들은 AddActivity 이동, 아이템 클릭시 나타나는 다이얼로그 등과 관련되어 있다.

 

마지막으로 AddActivity.kt의 코드이다.

class AddActivity : AppCompatActivity() {
    private val todoViewModel: MainViewModel by viewModels()
    private var id: Int? = null


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_add)

        if(intent!=null && intent.hasExtra(EXTRA_TODO_TITLE) && intent.hasExtra(EXTRA_TODO_DESC) && intent.hasExtra(
                EXTRA_TODO_ID)){
            add_edittext_title.setText(intent.getStringExtra(EXTRA_TODO_TITLE))
            add_edittext_descript.setText(intent.getStringExtra(EXTRA_TODO_DESC))
            id=intent.getIntExtra(EXTRA_TODO_ID, -1)
        }

        add_button.setOnClickListener {
            if(add_edittext_title.text.isNotEmpty() && add_edittext_descript.text.isNotEmpty()){
                val todo = Todo(id, add_edittext_title.text.toString(), add_edittext_descript.text.toString())
                lifecycleScope.launch(Dispatchers.IO){todoViewModel.insert(todo)}
                finish()
            }else{
                Toast.makeText(this,"Please enter title and desc", Toast.LENGTH_LONG).show()
            }
        }
    }

    companion object{
        const val EXTRA_TODO_TITLE = "EXTRA_TODO_TITLE"
        const val EXTRA_TODO_DESC = "EXTRA_TODO_DESC"
        const val EXTRA_TODO_ID = "EXTRA_TODO_ID"
    }
}

크게 설명할 부분은 없다. 다만 중요한 부분은 데이터를 편집하기 위해 코루틴을 이용하여 비동기적으로 처리하고있다. 

 

activity_add.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".AddActivity">
    <TextView
        android:text="일정"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/add_tv_title"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_marginStart="16dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.41000003"/>

    <EditText
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:inputType="textPersonName"
        android:ems="10"
        android:id="@+id/add_edittext_title"
        app:layout_constraintBottom_toBottomOf="@+id/add_tv_title"
        app:layout_constraintTop_toTopOf="@+id/add_tv_title"
        app:layout_constraintStart_toEndOf="@+id/add_tv_title"
        android:layout_marginStart="32dp"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginEnd="32dp"/>

    <TextView
        android:text="설명"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/add_tv_descript"
        android:layout_marginTop="40dp"
        app:layout_constraintTop_toBottomOf="@+id/add_tv_title"
        app:layout_constraintStart_toStartOf="@+id/add_tv_title"/>

    <EditText
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:ems="10"
        android:id="@+id/add_edittext_descript"
        app:layout_constraintStart_toStartOf="@+id/add_edittext_title"
        app:layout_constraintTop_toTopOf="@+id/add_tv_descript"
        app:layout_constraintBottom_toBottomOf="@+id/add_tv_descript"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginEnd="32dp"/>

    <Button
        android:text="done"
        android:layout_width="0dp"
        android:layout_height="49dp"
        android:id="@+id/add_button"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

참고로 AddActivity 부분은 편집을 위한 액티비티이기 때문에 특별한 데이터 바인딩이 불가능하다. (내가 모르는걸수도..)


리뷰

실제 구현을 하긴 했지만 다시 구현하라고 하면 복잡해서 시간이 좀 걸릴 것 같다. 그리고 몇몇 부분에 대한 추가적인 이해도 필요해보인다. Repository의 필요성, 기본적인 코틀린 문법 등을 학습할 필요를 느꼈다. 또한, 다른 코드들도 참조하여 내가 구현한 MVVM 패턴이 적절한지도 판단할 필요가 있다.

 

Github Repo : https://github.com/Gongcu/MVVMExample

참고 : https://blog.yena.io/studynote/2019/03/27/Android-MVVM-AAC-2.html