경험의 기록

2021.05.27 - [안드로이드/개발] - [Android 개발일지] MVVM 패턴으로 Todo, Done List 만들기 - (4) 구조 수정, 캘린더

 

[Android 개발일지] MVVM 패턴으로 Todo, Done List 만들기 - (4) 구조 수정, 캘린더

2021.05.24 - [안드로이드/개발] - [Android 개발일지] MVVM 패턴으로 Todo, Done List 만들기 - (3) Todo리싸이클러뷰, 메모 추가 기능 [Android 개발일지] MVVM 패턴으로 Todo, Done List 만들기 - (3) Todo리싸..

hanyeop.tistory.com

 

에서 이어지는 글입니다.


1️⃣ 데이터 바인딩

// 뷰 홀더에 데이터를 바인딩
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val currentItem = memoList[position]
        val currentContent = currentItem.content
        val currentYear = currentItem.year
        val currentMonth = currentItem.month
        val currentDay = currentItem.day

        val s_currentYear = currentYear.toString()
        var s_currentMonth = currentMonth.toString()
        var s_currentDay = currentDay.toString()

        if(currentMonth < 10) s_currentMonth = "0$currentMonth"
        if(currentDay < 10) s_currentDay = "0$currentDay"

        holder.binding.memoCheckBox.text = currentContent
        holder.binding.dateTextView.text = "$s_currentYear/$s_currentMonth/$s_currentDay"

    }

지금은 바인딩할때 일일히 데이터를 넣어주는 방식으로 화면을 보여주고 있는데,

데이터바인딩을 사용하여 실시간으로 데이터를 바인딩하여 갱신되도록 할 것이다.

 

2021.05.30 - [안드로이드/AAC, MVVM] - [Android] 리사이클러뷰에서 DataBinding, LiveData 사용하기 (+ BindingAdapter)

 

[Android] 리사이클러뷰에서 DataBinding, LiveData 사용하기 (+ BindingAdapter)

리사이클러뷰에서 데이터바인딩과 라이브데이터를 사용하여 리스트의 변화를 감지하여 바로 리사이클러뷰를 갱신해주려고 한다. 1️⃣ 데이터 바인딩 종속성 추가 buildFeatures { dataBinding true } de

hanyeop.tistory.com

위에서 다룬 방식으로 사용할 것이다.

 

데이터 바인딩 추가

buildFeatures {
        viewBinding true
        dataBinding true
    }

 

레이아웃 변경

<?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="memo"
            type="com.hanyeop.todoneList.model.Memo" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:background="@drawable/item_background"
        android:layout_margin="5dp">

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

            <CheckBox
                android:id="@+id/memoCheckBox"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:checked="@{memo.check}"
                android:text="@{memo.content}"
                android:textSize="16sp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/updateButton" />

            <Button
                android:id="@+id/updateButton"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:background="@drawable/ic_baseline_edit_24"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/deleteButton"
                app:layout_constraintTop_toTopOf="parent" />

            <Button
                android:id="@+id/deleteButton"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:background="@drawable/ic_baseline_delete_24"
                android:text="Button"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/dateTextView"
                android:gravity="right"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="16sp"
                android:textStyle="bold"
                android:text="@{@string/date_format(memo.year,memo.month,memo.day)}"
                android:layout_margin="4dp" />
        </LinearLayout>
    </LinearLayout>
</layout>

레이아웃 형태를 데이터바인딩을 위해 바꿔주고,

데이터클래스를 변수로 선언해주어

각각 정보에 해당하는 text들에 바인딩해준다.

 

여기서

<resources>
    <string name="date_format">%d/%d/%d</string>
</resources>

시간을 년/월/일 로 출력하기 위해 스트링 포맷을 따로 선언해주었다.

 

리사이클러뷰 어댑터

class TodoAdapter(private val memoViewModel: MemoViewModel) : RecyclerView.Adapter<TodoAdapter.MyViewHolder>() {

    private var memoList = emptyList<Memo>()

    // 뷰 홀더에 데이터를 바인딩
    class MyViewHolder(private val binding: TodoItemBinding) : RecyclerView.ViewHolder(binding.root){
        fun bind(currentMemo : Memo){
            binding.memo = currentMemo
        }
    }

    // 어떤 xml 으로 뷰 홀더를 생성할지 지정
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val binding = TodoItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
        return MyViewHolder(binding)
    }

    // 바인딩 함수로 넘겨줌
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(memoList[position])
    }

    // 뷰 홀더의 개수 리턴
    override fun getItemCount(): Int {
        return memoList.size
    }

    // 메모 리스트 갱신
    fun setData(memo : List<Memo>){
        memoList = memo
        notifyDataSetChanged()
    }

    // 아이템에 아이디를 설정해줌 (깜빡이는 현상방지)
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }
}

데이터바인딩으로 memo를 현재 아이템으로 변경해주어 연결해준다.

 

또한 갱신될때마다 notifyDataSetChanged가 실행되어 뷰가 다시 생성되어 화면이 깜빡이고 이미 있던 것들까지 생성하여 성능이 하락하므로

 

getItemId로 아이디를 만들어준다.

 

todolistFragment, calenderFragment

// 아이템에 아이디를 설정해줌 (깜빡이는 현상방지)
        adapter.setHasStableIds(true)

여기서 사용하는 어댑터에 아이디를 set해준다.

 

훨씬 코드가 깔끔해지고 의존성이 줄어들었다.

 

 

 

2️⃣ 메모 변경

// 바인딩 함수로 넘겨줌
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(memoList[position],memoViewModel)
    }

바인드뷰홀더에서 뷰모델도 파라미터로 넘겨주고

 

체크 변경, 삭제

// 뷰 홀더에 데이터를 바인딩
    class MyViewHolder(private val binding: TodoItemBinding) : RecyclerView.ViewHolder(binding.root){
        fun bind(currentMemo : Memo, memoViewModel: MemoViewModel){
            binding.memo = currentMemo

            // 체크 리스너 초기화 해줘 중복 오류 방지
            binding.memoCheckBox.setOnCheckedChangeListener(null)

            // 메모 체크 시 체크 데이터 업데이트
            binding.memoCheckBox.setOnCheckedChangeListener { _, check ->
                if (check) {
                    val memo = Memo(currentMemo.id, true, currentMemo.content,
                            currentMemo.year, currentMemo.month, currentMemo.day)
                    memoViewModel.updateMemo(memo)
                }
                else {
                    val memo = Memo(currentMemo.id, false, currentMemo.content,
                            currentMemo.year, currentMemo.month, currentMemo.day)
                    memoViewModel.updateMemo(memo)
                }
            }

            // 삭제 버튼 클릭 시 메모 삭제
            binding.deleteButton.setOnClickListener {
                memoViewModel.deleteMemo(currentMemo)
            }
        }
    }

체크 여부에 따라 체크 데이터를 update로 갱신해주고,

삭제 기능은 delete로 쉽게 구현할 수 있다.

 

 

내용 수정

class UpdateDialog(context : Context, updateDialogInterface : UpdateDialogInterface) : Dialog(context) {

    // 액티비티에서 인터페이스를 받아옴
    private var updateDialogInterface: UpdateDialogInterface = updateDialogInterface

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

        var okButton : Button = findViewById(R.id.okButton)
        var cancelButton : Button = findViewById(R.id.cancelButton)
        var memoEditView : EditText = findViewById(R.id.memoEditView)

        // 배경 투명하게 바꿔줌
        window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))

        okButton.setOnClickListener {
            val content = memoEditView.text.toString()

            // 입력하지 않았을 때
            if ( TextUtils.isEmpty(content)){
                Toast.makeText(context, "수정할 내용을 입력해주세요.", Toast.LENGTH_SHORT).show()
            }

            // 입력 창이 비어 있지 않을 때
            else{
                // 메모를 수정해줌
                updateDialogInterface.onOkButtonClicked(content)
                dismiss()
            }
        }

        // 취소 버튼 클릭 시 종료
        cancelButton.setOnClickListener { dismiss()}
    }
}

메모 추가시 사용한 다이얼로그와 유사한 다이얼로그를 만들어주고

 

interface UpdateDialogInterface {
    fun onOkButtonClicked(content : String)
}

인터페이스도 생성해준다.

 

    class MyViewHolder(private val binding: TodoItemBinding) : RecyclerView.ViewHolder(binding.root),
            UpdateDialogInterface{
        lateinit var memo : Memo
        lateinit var memoViewModel: MemoViewModel

        fun bind(currentMemo : Memo, memoViewModel: MemoViewModel){
            binding.memo = currentMemo
            this.memoViewModel = memoViewModel

            // 체크 리스너 초기화 해줘 중복 오류 방지
            binding.memoCheckBox.setOnCheckedChangeListener(null)

            // 메모 체크 시 체크 데이터 업데이트
            binding.memoCheckBox.setOnCheckedChangeListener { _, check ->
                if (check) {
                    memo = Memo(currentMemo.id, true, currentMemo.content,
                            currentMemo.year, currentMemo.month, currentMemo.day)
                    this.memoViewModel.updateMemo(memo)
                }
                else {
                    memo = Memo(currentMemo.id, false, currentMemo.content,
                            currentMemo.year, currentMemo.month, currentMemo.day)
                    this.memoViewModel.updateMemo(memo)
                }
            }

            // 삭제 버튼 클릭 시 메모 삭제
            binding.deleteButton.setOnClickListener {
                memoViewModel.deleteMemo(currentMemo)
            }

            // 수정 버튼 클릭 시 다이얼로그 띄움
            binding.updateButton.setOnClickListener {
                memo = currentMemo
                val myCustomDialog = UpdateDialog(binding.updateButton.context,this)
                myCustomDialog.show()
            }
        }

        // 다이얼로그의 결과값으로 업데이트 해줌
        override fun onOkButtonClicked(content: String) {
            val updateMemo = Memo(memo.id,memo.check,content,memo.year,memo.month,memo.day)
            memoViewModel.updateMemo(updateMemo)
        }
    }

수정 버튼 클릭시 다이얼로그를 띄워

인터페이스로 구현해 데이터를 공유하여 받아온다.

 

 

 

3️⃣ 오류 수정

dao

@Query("SELECT * FROM Memo WHERE year = :year AND month = :month AND day = :day ORDER BY id DESC")
    fun readDateData(year : Int, month : Int, day : Int) : Flow<List<Memo>>

repository

 fun readDateData(year : Int, month : Int, day : Int): Flow<List<Memo>> { 
        return memoDao.readDateData(year, month, day)
    }

viewmodel

fun readDateData(year : Int, month : Int, day : Int): LiveData<List<Memo>> {
        return repository.readDateData(year, month, day).asLiveData()
        }

프래그먼트

binding!!.calendarView.setOnDateChangeListener { _, year, month, day ->
            // 날짜 선택시 그 날의 정보 할당
            this.year = year
            this.month = month+1
            this.day = day

            binding!!.calendarDateText.text = "${this.year}/${this.month}/${this.day}"

           // 리스트 관찰하여 변경시 어댑터에 전달해줌
            memoViewModel.readDateData(this.year,this.month,this.day).observe(viewLifecycleOwner, Observer {
                adapter.setData(it)
            })nth,this.day)
        }

기존의 방식을 보면

날짜를 변경할때마다 새로운 옵저버를 만들게되어

여러 옵저버가 생성될 경우 중복호출되는 치명적인 버그가 발생하여

캘린더프래그먼트의 리사이클러뷰 아이템들이 꼬이는 버그가 발생했다.

 

 

 

그래서 며칠동안 해결하기위해 헤메었는데, 

정답이라고 할수는 없지만 나름대로 로직을 만들어 구현해보았다.

 

dao

@Query("SELECT * FROM Memo WHERE year = :year AND month = :month AND day = :day ORDER BY id DESC")
fun readDateData(year : Int, month : Int, day : Int) : List<Memo>

repository

 

fun readDateData(year : Int, month : Int, day : Int): List<Memo> {
        return memoDao.readDateData(year, month, day)
    }

viewmodel

 // get set
    private var _currentData = MutableLiveData<List<Memo>>()
    val currentData : LiveData<List<Memo>>
        get() = _currentData
        
         fun readDateData(year : Int, month : Int, day : Int) {
        viewModelScope.launch(Dispatchers.IO) {
            val tmp = repository.readDateData(year, month, day)
            _currentData.postValue(tmp)
        }
    }

반환 값을 List로 바꿔주고

라이브데이터를 하나 선언하여

readDateData 함수가 호출될 때 마다 그 날에 맞는 라이브데이터를 갱신시키는 방식으로 변경하였다.

 

프래그먼트

binding!!.calendarView.setOnDateChangeListener { _, year, month, day ->
            // 날짜 선택시 그 날의 정보 할당
            this.year = year
            this.month = month+1
            this.day = day

            binding!!.calendarDateText.text = "${this.year}/${this.month}/${this.day}"

            // 해당 날짜 데이터를 불러옴 (currentData 변경)
            memoViewModel.readDateData(this.year,this.month,this.day)
        }

        // 메모 데이터가 수정되었을 경우 날짜 데이터를 불러옴 (currentData 변경)
        memoViewModel.readAllData.observe(viewLifecycleOwner, {
            memoViewModel.readDateData(year, month, day)
        })

        // 현재 날짜 데이터 리스트(currentData) 관찰하여 변경시 어댑터에 전달해줌
        memoViewModel.currentData.observe(viewLifecycleOwner, Observer {
            adapter.setData(it)
            Log.d("test5", "onCreateView: gg")
        })

날짜 선택 시 readDateData 함수를 호출하여 값을 변경해주고,

그 변경된 값을 감지하여 리사이클러뷰가 갱신되도록 하였다.

 

또한 전체메모리스트도 관찰하여

변경 시 readDateData 함수를 호출하도록 하였다.

 


이제 doneList를 만들고 전체적으로 마무리하면 될 것 같다.

 

https://github.com/HanYeop/TodoneList

 

HanYeop/TodoneList

Todo-Done List . Contribute to HanYeop/TodoneList development by creating an account on GitHub.

github.com

 

반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading