그냥 가끔의 기록장

[Kotlin] Todo 토이 프로젝트 [3단계] (ViewPager2 + RecyclerView + ViewModel) 본문

Android

[Kotlin] Todo 토이 프로젝트 [3단계] (ViewPager2 + RecyclerView + ViewModel)

the phoenix 2022. 4. 30. 20:17

https://soeun-87.tistory.com/33

 

[Kotlin] Todo 토이 프로젝트 [2단계] (ViewPager2 + RecyclerView + ViewModel)

https://soeun-87.tistory.com/32 [Kotlin] Todo 토이 프로젝트 [1단계] (ViewPager2 + RecyclerView + ViewModel) 아주 오랜만에 블로그에 글을 쓰는데, 최근에 진행한 Todo 토이 프로젝트를 순서대로 작성해볼까..

soeun-87.tistory.com

앞 글에 이어 3단계로 토이 프로젝트를 완성해보자.

 

1. 단계별 코드

(1) Adapter 2개를 1개로 다시 통일

(간단한 프로젝트고, 이전 2단계에서 사용한 RecyclerView의 Adapter 2개 코드가 완전 동일해서 하나로 통일 시켰다. 즉, 2개의 Recyclerview가 1개의 일종의 Base Adapter를 사용한다 보면 됨)

 

  DoneFragment와 PendingFragment에선 동일한 로직의 Adapter를 쓰므로 이전 포스팅에서 만든 2개의 Adapter를 TodoAdapter 한개로 통일시킨다. 대신 DoneFragment에선 checkBox가 check된 doneData만 보여주고, PendingFragment에선 checkBox가 check가 안 된 pendingData만 보여주기 위해 setDoneData(), setPendingData() 함수 2개를 추가했다. 

 

1) TodoAdapter.kt

package com.example.kotlintodo.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.kotlintodo.databinding.ItemTodoBinding
import com.example.kotlintodo.model.Todo

class TodoAdapter(var Todos: List<Todo>): RecyclerView.Adapter<TodoAdapter.ToDoViewHolder>() {

    private lateinit var itemBinding: ItemTodoBinding

    inner class ToDoViewHolder(private val itemBinding: ItemTodoBinding): RecyclerView.ViewHolder(itemBinding.root){
        fun bind(data:Todo) {
            itemBinding.tvTitle.text = data.title
            itemBinding.tvContent.text = data.content
            itemBinding.cbIsDone.isChecked = data.isDone
        }
    }

    // RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - viewHolder에 layout inflate 하는 함수 (ViewBinding 사용)
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ToDoViewHolder {
        itemBinding = ItemTodoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ToDoViewHolder(itemBinding)
    }

    // RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - viewHolder에 각 view를 bind하는 함수
    // ToDoViewHolder내에 bind함수 정의했으므로, 각 Todos[position]인 item data랑 view를 bind하면 됨
    override fun onBindViewHolder(holder: ToDoViewHolder, position: Int) {
        holder.bind(Todos[position])
    }

    // RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - 보통 Todos.size를 return, RecyclerView내의 item 개수 return하는 함수
    override fun getItemCount(): Int {
        return Todos.size
    }

    // Todos list의 각 item id return
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    fun setDoneData(doneData: List<Todo>){
        Todos = doneData
        notifyDataSetChanged()
    }

    fun setPendingData(pendingData: List<Todo>){
        Todos = pendingData
        notifyDataSetChanged()
    }
}

 

 

(2) TodoViewModel 추가

  이 프로젝트에서 viewmodel을 사용한 건, MVVM 패턴을 구현하려고 한 것은 아니고 여러 Fragment들 간에 공통된 데이터를 공유하게끔 하기 위해 가장 간단한 방법인 viewmodel을 사용한 것이다. (즉, create한 item을 다른 Fragment에 bundle로 보내기 싫어서.. liveData를 갖는 viewModel을 만들어서 여러 Fragment들 간에 동일한 데이터를 공유하게 하려고 사용한 것이다)

 

MutuableLiveData인 pendingList와 doneList 2개를 viewModel 안에 정의한다. 두 종류의 LiveData를 합쳐서 갖고 있는 todoList는 Mediator로 정의한다. (여기선 Medaiator를 설명하면 너무 글이 길어질 것 같아 참고할 링크를 첨부하였다.)

 

viewModel을 간단히 구현한 후, PendingFragment와 DoneFragment에 viewModel을 추가한다. 여기서 주의할 점은, Fragment의 경우 onViewCreated를 override해서 여기서 viewModel의 liveData를 observe해야 한다는 점이다. (즉 view인 Fragment가 viewModel의 liveData를 observe해서, liveData가 변하면 view인 fragment의 UI가 자동으로 업데이트 된다는 건데.. 이 역시 MVVM 패턴과 관련되므로 따로 포스팅 하려 한다.)

 

  이 토이 프로젝트는 다시 말하지만 MVVM을 다루려는게 아니라 ViewPager2 + RecyclerView가 주가 되는 프로젝트라, observe하는 부분의 코드를 당장 깊게 이해할 필요는 없을 것 같다. observe 함수 내에서 filter를 쓰는건 isDone의 true, false 값에 따라 pendingList, doneList가 나뉘기 때문에 사용한 것이다. (즉, todoList는 isDone이 true, false인 애들이 모두 섞여있는데 그 중 filtering을 해서 true인 애들만 모아 DoneFragment에서 보여주고 false인 애들만 모아 PendingFragment에서 보여주려고 한 것이다.)

 

1) TodoViewModel.kt

package com.example.kotlintodo.ui

import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.kotlintodo.model.Todo

class TodoViewModel : ViewModel() {

    val todoList = MediatorLiveData<List<Todo>>()
    private var datas = mutableListOf<Todo>()
    val doneList = MutableLiveData<List<Todo>>()
    val pendingList = MutableLiveData<List<Todo>>()

    init{
        todoList.addSource(pendingList){
            value->todoList.value = value
        }
        todoList.addSource(doneList){
            value -> todoList.value = value
        }
    }

    private fun setData(data: ArrayList<Todo>){
        pendingList.value = data.filter { x-> !x.isDone }.toList()
        doneList.value = data.filter { x->x.isDone }.toList()
        todoList.value = data
    }
}

 

2) PendingFragment.kt

package com.example.kotlintodo.ui.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kotlintodo.adapter.TodoAdapter
import com.example.kotlintodo.databinding.FragmentPendingBinding
import com.example.kotlintodo.model.Todo
import com.example.kotlintodo.ui.TodoViewModel

class PendingFragment: Fragment() {

    private lateinit var viewModel: TodoViewModel
    private lateinit var binding: FragmentPendingBinding
    private lateinit var adapter : TodoAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 1. View Model 설정
        viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory()) .get(
            TodoViewModel::class.java)

        // 2. View Binding 설정
        binding = FragmentPendingBinding.inflate(inflater, container, false)

        // 3. adapter 설정 (list를 인자로)
        var pendingList = viewModel.pendingList.value
        adapter = TodoAdapter(pendingList?: emptyList<Todo>())
        adapter.setHasStableIds(true)
        binding!!.rvPending.adapter = adapter

        // 4. recyclerView에 Layout 꼭 설정하기 (안그러면 화면에 표시 안되고 skip됨)
        binding!!.rvPending.layoutManager = LinearLayoutManager(activity)

        // 5. return Fragment Layout View
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.todoList.observe(viewLifecycleOwner, Observer{
            binding.rvPending.post(Runnable { it.filter { x -> !x.isDone } })
        })
    }
}

 

3) DoneFragment.kt

package com.example.kotlintodo.ui.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kotlintodo.adapter.TodoAdapter
import com.example.kotlintodo.databinding.FragmentDoneBinding
import com.example.kotlintodo.model.Todo
import com.example.kotlintodo.ui.TodoViewModel

class DoneFragment: Fragment() {

    private lateinit var viewModel: TodoViewModel
    private lateinit var binding: FragmentDoneBinding
    private lateinit var adapter : TodoAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 1. View Model 설정
        viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory()) .get(
            TodoViewModel::class.java)

        // 2. View Binding 설정
        binding = FragmentDoneBinding.inflate(inflater, container, false)

        // 3. adapter 설정 (list를 인자로)
        var doneList = viewModel.doneList.value
        adapter = TodoAdapter(doneList?: emptyList<Todo>())
        adapter.setHasStableIds(true)
        binding!!.rvDone.adapter = adapter

        // 4. recyclerView에 Layout 꼭 설정하기 (안그러면 화면에 표시 안되고 skip됨)
        binding!!.rvDone.layoutManager = LinearLayoutManager(activity)

        // 5. return Fragment Layout View
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.todoList.observe(viewLifecycleOwner, Observer{
            binding.rvDone.post(Runnable { adapter.setDoneData(it.filter { x->x.isDone }) })
        })
    }
}

 

 

 

(3) CRUD 중 Create 기능 추가

  이번에는 item을 동적으로 Create해서 recylerView에 Add하는 기능을 추가해보자. 순서대로 따라하면 된다.

 

1. fragment_add.xml 생성

2. AddFragment.kt 생성

3. viewModel에 addTask() 함수 추가

4. PendingFragment 수정 

 

  1, 2번은 당연히 해야 하는거고 새로 Fragment 세트를 만드는 것이라 보면 된다. 3번은 viewModel의 liveData에 새로 추가한 item을 추가하기 위해 함수를 새로 만든 것이다. 4번의 경우 PendingFragment의 btnAddTask에 setOnClickListener를 추가하려는 것이다. 자세한 설명은 각 코드의 주석을 참고하면 된다. 

 

1) fragment_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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/etTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="124dp"
        android:layout_marginRight="20dp"
        android:hint="Title"
        android:minHeight="48dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/etContent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="44dp"
        android:layout_marginRight="20dp"
        android:hint="Content"
        android:minHeight="48dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.6"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/etTitle" />

    <Button
        android:id="@+id/btnAdd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:text="ADD TASK"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

2) AddFragment.kt

package com.example.kotlintodo.ui.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.example.kotlintodo.R
import com.example.kotlintodo.databinding.FragmentAddBinding
import com.example.kotlintodo.model.Todo
import com.example.kotlintodo.ui.MainActivity
import com.example.kotlintodo.ui.TodoViewModel

class AddFragment: Fragment() {

    private lateinit var viewModel: TodoViewModel
    private lateinit var binding: FragmentAddBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 1. View Model 설정
        viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory()) .get(
            TodoViewModel::class.java)
        // 2. View Binding 설정
        binding = FragmentAddBinding.inflate(inflater, container, false)
        // 3. return Fragment Layout View
        return binding.root
    }

    // Add Fragment -> Home Fragment intent && viewmodel addTask 원활하게 호출하기 위해
    // onStart (Activity 만들어진 후, 사용자에게 보여지는 시점) 에서 Add Button에 onClickListener 추가
    override fun onStart() {
        super.onStart()
        binding.btnAdd.setOnClickListener{
            val title = binding.etTitle.text.toString()
            val content = binding.etContent.text.toString()

            // 새로운 item 생성해서 viewmodel의 addTask 호출
            viewModel.addTask(Todo(title, content, false))

            // Add Fragment -> Home Fragment로 intent
            val transaction = (activity as MainActivity).supportFragmentManager.beginTransaction()
            transaction.replace(R.id.frameLayout, HomeFragment())
            transaction.disallowAddToBackStack()
            transaction.commit()
        }
    }
}

 

3) TodoViewModel.kt

package com.example.kotlintodo.ui

import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.kotlintodo.model.Todo

class TodoViewModel : ViewModel() {

    val todoList = MediatorLiveData<List<Todo>>()
    private var datas = arrayListOf<Todo>()
    val doneList = MutableLiveData<List<Todo>>()
    val pendingList = MutableLiveData<List<Todo>>()

    init{
        todoList.addSource(pendingList){
            value->todoList.value = value
        }
        todoList.addSource(doneList){
            value -> todoList.value = value
        }
    }

    fun addTask(todo: Todo){
        datas.add(todo)
        setData(datas)
    }

    private fun setData(data: ArrayList<Todo>){
        pendingList.value = data.filter { x-> !x.isDone }.toList()
        doneList.value = data.filter { x->x.isDone }.toList()
        todoList.value = data
    }
}

 

4) PendingFragment.kt

package com.example.kotlintodo.ui.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kotlintodo.R
import com.example.kotlintodo.adapter.TodoAdapter
import com.example.kotlintodo.databinding.FragmentPendingBinding
import com.example.kotlintodo.model.Todo
import com.example.kotlintodo.ui.MainActivity
import com.example.kotlintodo.ui.TodoViewModel

class PendingFragment: Fragment() {

    private lateinit var viewModel: TodoViewModel
    private lateinit var binding: FragmentPendingBinding
    private lateinit var adapter : TodoAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 1. View Model 설정
        viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory()) .get(
            TodoViewModel::class.java)

        // 2. View Binding 설정
        binding = FragmentPendingBinding.inflate(inflater, container, false)

        // 3. adapter 설정 (list를 인자로)
        var pendingList = viewModel.pendingList.value
        adapter = TodoAdapter(pendingList?: emptyList<Todo>())
        adapter.setHasStableIds(true)
        binding!!.rvPending.adapter = adapter

        // 4. recyclerView에 Layout 꼭 설정하기 (안그러면 화면에 표시 안되고 skip됨)
        binding!!.rvPending.layoutManager = LinearLayoutManager(activity)

        // 5. return Fragment Layout View
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.todoList.observe(viewLifecycleOwner, Observer{
            binding.rvPending.post(Runnable { it.filter { x -> !x.isDone } })
        })
    }

    override fun onStart() {
        super.onStart()
        binding.btnAddTask.setOnClickListener{
            val transaction = (activity as MainActivity).supportFragmentManager.beginTransaction()
            transaction.replace(R.id.frameLayout, AddFragment())
            transaction.commit()
        }
    }
}

 

* 결과

- Add가 잘 되는걸 확인할 수 있다. 

 

(4) CRUD 중 Delete 기능 추가

 이번에는 item을 동적으로 Delete해서 recylerView에서 Remove하는 기능을 추가해보자. 순서대로 따라하면 된다.

 

1. item_todo.xml 수정 - delete 버튼 추가 

2. TodoAdapter.kt 수정 - delete관련 함수 생성자에 추가하기 

3. viewModel에 deleteTask() 함수 추가

4. PendingFragment, DoneFragment 수정 - Delete 함수 생성 및 Adapter 생성자 인자로 넘기기 

 

  이번에는 Create 부분과 좀 다른데, Adapter의 생성자에 delete하는 함수를 추가하여 viewHolder class의 bind함수에서 해당 delete 함수를 호출한다. 이를 위해선 3, 4번이 되어야 하는데, viewModel의 liveData에 item을 remove하기 위해 함수를 추가해야 하고, PendingFragment와 DoneFragment에서 Adapter 생성 시 delete함수를 인자로 넘겨야 한다.

자세한 설명은 각 코드의 주석을 참고하면 된다. 

 

1) item_todo.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="100dp">

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:textSize="24sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/tvContent"
        app:layout_constraintEnd_toStartOf="@+id/cbIsDone"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="title" />

    <TextView
        android:id="@+id/tvContent"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/btnDelete"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvTitle"
        tools:text="content" />

    <CheckBox
        android:id="@+id/cbIsDone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@+id/btnDelete"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/tvTitle"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnDelete"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="DELETE"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/cbIsDone" />

</androidx.constraintlayout.widget.ConstraintLayout>

결과:

 

2) TodoAdapter.kt

package com.example.kotlintodo.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.kotlintodo.databinding.ItemTodoBinding
import com.example.kotlintodo.model.Todo

class TodoAdapter(var Todos: List<Todo>, val onClickDeleteButton: (todo: Todo) -> Unit): RecyclerView.Adapter<TodoAdapter.ToDoViewHolder>() {

    private lateinit var itemBinding: ItemTodoBinding

    inner class ToDoViewHolder(private val itemBinding: ItemTodoBinding): RecyclerView.ViewHolder(itemBinding.root){
        fun bind(data:Todo) {
            itemBinding.tvTitle.text = data.title
            itemBinding.tvContent.text = data.content
            itemBinding.cbIsDone.isChecked = data.isDone

            // delete 추가
            itemBinding.btnDelete.setOnClickListener {
                onClickDeleteButton.invoke(data)
            }
        }
    }

    // RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - viewHolder에 layout inflate 하는 함수 (ViewBinding 사용)
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ToDoViewHolder {
        itemBinding = ItemTodoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ToDoViewHolder(itemBinding)
    }

    // RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - viewHolder에 각 view를 bind하는 함수
    // ToDoViewHolder내에 bind함수 정의했으므로, 각 Todos[position]인 item data랑 view를 bind하면 됨
    override fun onBindViewHolder(holder: ToDoViewHolder, position: Int) {
        holder.bind(Todos[position])
    }

    // RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - 보통 Todos.size를 return, RecyclerView내의 item 개수 return하는 함수
    override fun getItemCount(): Int {
        return Todos.size
    }

    // Todos list의 각 item id return
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    fun setDoneData(doneData: List<Todo>){
        Todos = doneData
        notifyDataSetChanged()
    }

    fun setPendingData(pendingData: List<Todo>){
        Todos = pendingData
        notifyDataSetChanged()
    }
}

 

3) TodoViewModel.kt

package com.example.kotlintodo.ui

import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.kotlintodo.model.Todo

class TodoViewModel : ViewModel() {

    val todoList = MediatorLiveData<List<Todo>>()
    private var datas = arrayListOf<Todo>()
    val doneList = MutableLiveData<List<Todo>>()
    val pendingList = MutableLiveData<List<Todo>>()

    init{
        todoList.addSource(pendingList){
            value->todoList.value = value
        }
        todoList.addSource(doneList){
            value -> todoList.value = value
        }
    }

    fun addTask(todo: Todo){
        datas.add(todo)
        setData(datas)
    }

    fun deleteTask(todo:Todo){
        datas.remove(todo)
        setData(datas)
    }

    private fun setData(data: ArrayList<Todo>){
        pendingList.value = data.filter { x-> !x.isDone }.toList()
        doneList.value = data.filter { x->x.isDone }.toList()
        todoList.value = data
    }
}

 

4) PendingFragment.kt

package com.example.kotlintodo.ui.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kotlintodo.R
import com.example.kotlintodo.adapter.TodoAdapter
import com.example.kotlintodo.databinding.FragmentPendingBinding
import com.example.kotlintodo.model.Todo
import com.example.kotlintodo.ui.MainActivity
import com.example.kotlintodo.ui.TodoViewModel

class PendingFragment: Fragment() {

    private lateinit var viewModel: TodoViewModel
    private lateinit var binding: FragmentPendingBinding
    private lateinit var adapter : TodoAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // 1. View Model 설정
        viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory()) .get(
            TodoViewModel::class.java)

        // 2. View Binding 설정
        binding = FragmentPendingBinding.inflate(inflater, container, false)

        // 3. adapter 설정 (list를 인자로)
        var pendingList = viewModel.pendingList.value
        adapter = TodoAdapter(pendingList?: emptyList<Todo>(),
            onClickDeleteButton={ viewModel.deleteTask(it)})
        adapter.setHasStableIds(true)
        binding.rvPending.adapter = adapter

        // 4. recyclerView에 Layout 꼭 설정하기 (안그러면 화면에 표시 안되고 skip됨)
        binding.rvPending.layoutManager = LinearLayoutManager(activity)

        // 5. return Fragment Layout View
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.todoList.observe(viewLifecycleOwner, Observer{
            binding.rvPending.post(Runnable { adapter.setPendingData(it.filter { x -> !x.isDone }) })
        })
    }

    override fun onStart() {
        super.onStart()
        binding.btnAddTask.setOnClickListener{
            val transaction = (activity as MainActivity).supportFragmentManager.beginTransaction()
            transaction.replace(R.id.frameLayout, AddFragment())
            transaction.commit()
        }
    }
}

 

5) DoneFragment.kt

package com.example.kotlintodo.ui.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kotlintodo.adapter.TodoAdapter
import com.example.kotlintodo.databinding.FragmentDoneBinding
import com.example.kotlintodo.model.Todo
import com.example.kotlintodo.ui.TodoViewModel

class DoneFragment: Fragment() {

    private lateinit var viewModel: TodoViewModel
    private lateinit var binding: FragmentDoneBinding
    private lateinit var adapter : TodoAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // 1. View Model 설정
        viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory()) .get(
            TodoViewModel::class.java)

        // 2. View Binding 설정
        binding = FragmentDoneBinding.inflate(inflater, container, false)

        // 3. adapter 설정
        var doneList = viewModel.doneList.value
        adapter = TodoAdapter(doneList?: emptyList<Todo>(),
             onClickDeleteButton={ viewModel.deleteTask(it)})
        adapter.setHasStableIds(true)
        binding.rvDone.adapter = adapter

        // 4. recyclerView에 Layout 꼭 설정하기 (안그러면 화면에 표시 안되고 skip됨)
        binding.rvDone.layoutManager = LinearLayoutManager(activity)

        // 5. return Fragment Layout View
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.todoList.observe(viewLifecycleOwner, Observer{
            binding.rvDone.post(Runnable { adapter.setDoneData(it.filter { x->x.isDone }) })
        })
    }
}

 

*결과 - delete 잘 되는 것 확인할 수 있다. 

 

 

 

 

(5) CRUD 중 Update 기능 추가(check box update만, title이나 content update는 이 프로젝트에선 진행하지 않음) 

  마지막으로, item의 checkBox를 클릭해서 true가 되면 완료된 Todo로 보고 DoneFragment로, checkBox를 다시 해제하면 PendingFragment로 item들을 이동시켜보자. 다행히 앞의 delete와 아주 똑같은 방식으로 진행된다. 

 

1. TodoAdapter.kt 수정 - update 관련 함수 생성자에 추가하기 

3. viewModel에 updateToggle() 함수 추가

4. PendingFragment, DoneFragment 수정 - update 함수 생성 및 Adapter 생성자 인자로 넘기기 

 

1) TodoAdapter.kt

package com.example.kotlintodo.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.kotlintodo.databinding.ItemTodoBinding
import com.example.kotlintodo.model.Todo

class TodoAdapter(var Todos: List<Todo>,
                  val onClickDeleteButton: (todo: Todo) -> Unit,
                  val onCheckedChange: (todo: Todo, isCheck: Boolean) -> Unit):
    RecyclerView.Adapter<TodoAdapter.ToDoViewHolder>() {

    private lateinit var itemBinding: ItemTodoBinding

    inner class ToDoViewHolder(private val itemBinding: ItemTodoBinding): RecyclerView.ViewHolder(itemBinding.root){
        fun bind(data:Todo) {
            itemBinding.tvTitle.text = data.title
            itemBinding.tvContent.text = data.content
            // update toggle이 되지 않아 함수를 null로 먼저 초기화
            itemBinding.cbIsDone.setOnCheckedChangeListener(null)
            itemBinding.cbIsDone.isChecked = data.isDone

            // delete 추가
            itemBinding.btnDelete.setOnClickListener {
                onClickDeleteButton.invoke(data)
            }
            // update toggle 다시 추가
            itemBinding.cbIsDone.setOnCheckedChangeListener { _, isChecked ->
                onCheckedChange.invoke(data, isChecked)
            }
        }
    }

    // RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - viewHolder에 layout inflate 하는 함수 (ViewBinding 사용)
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ToDoViewHolder {
        itemBinding = ItemTodoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ToDoViewHolder(itemBinding)
    }

    // RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - viewHolder에 각 view를 bind하는 함수
    // ToDoViewHolder내에 bind함수 정의했으므로, 각 Todos[position]인 item data랑 view를 bind하면 됨
    override fun onBindViewHolder(holder: ToDoViewHolder, position: Int) {
        holder.bind(Todos[position])
    }

    // RecyclerView.Adapter 상속 시 무조건 override 해야하는 fun - 보통 Todos.size를 return, RecyclerView내의 item 개수 return하는 함수
    override fun getItemCount(): Int {
        return Todos.size
    }

    // Todos list의 각 item id return
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    fun setDoneData(doneData: List<Todo>){
        Todos = doneData
        notifyDataSetChanged()
    }

    fun setPendingData(pendingData: List<Todo>){
        Todos = pendingData
        notifyDataSetChanged()
    }
}

 

2) TodoViewModel.kt

package com.example.kotlintodo.ui

import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.kotlintodo.model.Todo

class TodoViewModel : ViewModel() {

    val todoList = MediatorLiveData<List<Todo>>()
    private var datas = arrayListOf<Todo>()
    val doneList = MutableLiveData<List<Todo>>()
    val pendingList = MutableLiveData<List<Todo>>()

    init{
        todoList.addSource(pendingList){
            value->todoList.value = value
        }
        todoList.addSource(doneList){
            value -> todoList.value = value
        }
    }

    fun addTask(todo: Todo){
        datas.add(todo)
        setData(datas)
    }

    fun deleteTask(todo:Todo){
        datas.remove(todo)
        setData(datas)
    }

    fun updateToggle(todo:Todo, isCheck: Boolean) {
        if (todo.isDone != isCheck) {
            todo.isDone = isCheck
        }
        setData(datas)
    }

    private fun setData(data: ArrayList<Todo>){
        pendingList.value = data.filter { x-> !x.isDone }.toList()
        doneList.value = data.filter { x->x.isDone }.toList()
        todoList.value = data
    }
}

 

3) PendingFragment.kt

package com.example.kotlintodo.ui.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kotlintodo.R
import com.example.kotlintodo.adapter.TodoAdapter
import com.example.kotlintodo.databinding.FragmentPendingBinding
import com.example.kotlintodo.model.Todo
import com.example.kotlintodo.ui.MainActivity
import com.example.kotlintodo.ui.TodoViewModel

class PendingFragment: Fragment() {

    private lateinit var viewModel: TodoViewModel
    private lateinit var binding: FragmentPendingBinding
    private lateinit var adapter : TodoAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // 1. View Model 설정
        viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory()) .get(
            TodoViewModel::class.java)

        // 2. View Binding 설정
        binding = FragmentPendingBinding.inflate(inflater, container, false)

        // 3. adapter 설정 (list를 인자로)
        var pendingList = viewModel.pendingList.value
        adapter = TodoAdapter(
            pendingList?: emptyList<Todo>(),
            onClickDeleteButton={
                viewModel.deleteTask(it) },
            onCheckedChange ={ it:Todo, check:Boolean ->
                viewModel.updateToggle(it, check)
            }
        )
        adapter.setHasStableIds(true)
        binding.rvPending.adapter = adapter

        // 4. recyclerView에 Layout 꼭 설정하기 (안그러면 화면에 표시 안되고 skip됨)
        binding.rvPending.layoutManager = LinearLayoutManager(activity)

        // 5. return Fragment Layout View
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.todoList.observe(viewLifecycleOwner, Observer{
            binding.rvPending.post(Runnable { adapter.setPendingData(it.filter { x -> !x.isDone }) })
        })
    }

    override fun onStart() {
        super.onStart()
        binding.btnAddTask.setOnClickListener{
            val transaction = (activity as MainActivity).supportFragmentManager.beginTransaction()
            transaction.replace(R.id.frameLayout, AddFragment())
            transaction.commit()
        }
    }
}

 

4) Donefragent.kt

package com.example.kotlintodo.ui.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kotlintodo.adapter.TodoAdapter
import com.example.kotlintodo.databinding.FragmentDoneBinding
import com.example.kotlintodo.model.Todo
import com.example.kotlintodo.ui.TodoViewModel

class DoneFragment: Fragment() {

    private lateinit var viewModel: TodoViewModel
    private lateinit var binding: FragmentDoneBinding
    private lateinit var adapter : TodoAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // 1. View Model 설정
        viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory()) .get(
            TodoViewModel::class.java)

        // 2. View Binding 설정
        binding = FragmentDoneBinding.inflate(inflater, container, false)

        // 3. adapter 설정
        var doneList = viewModel.doneList.value
        adapter = TodoAdapter(
            doneList?: emptyList<Todo>(),
             onClickDeleteButton={
                 viewModel.deleteTask(it) },
             onCheckedChange ={ it:Todo, check:Boolean ->
                viewModel.updateToggle(it, check)
             }
        )
        adapter.setHasStableIds(true)
        binding.rvDone.adapter = adapter

        // 4. recyclerView에 Layout 꼭 설정하기 (안그러면 화면에 표시 안되고 skip됨)
        binding.rvDone.layoutManager = LinearLayoutManager(activity)

        // 5. return Fragment Layout View
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.todoList.observe(viewLifecycleOwner, Observer{
            binding.rvDone.post(Runnable { adapter.setDoneData(it.filter { x->x.isDone }) })
        })
    }
}

 

*결과 - item들이 check 여부에 따라 잘 이동됨을 확인할 수 있다. 

 

 

쓰다보니 CRUD를 추가해서 글이 길어졌는데, 아래 GitHub에 순서대로 commit 했으므로,  commit 단위별 코드를 순서대로 참고하면 될 것 같다.

Comments