1차 제출 코드 전문은 깃허브 Android_ch3_MyImgSearch의 listAdatper 브런치 참조
일단 수요일에는.. 리스트어댑터 문제라고 생각했던 모든 문제들이... 리스트어댑터 문제가 아니였다..!! 뷰모델에서 데이터 관리를 어떻게 하느냐의 문제였다. 그래서 튜터님을 방문해서, 뷰모델의 관한 보충을 조금 들었다.
=> 일단 뷰모델은 데이터를 공유하기 위해 만드는 sharedViewModel이 하나 있고, 하나의 프래그먼트의 데이터 변화를 편하게 관찰하기 위한 개별뷰모델이 하나 있다. 는게 포인트였다. 요지는? 나는 처음에, 그냥 dataList 하나로 가져와서 여기에 필터를 걸어서 isliked가 true인 값만 보관함에 보이게 만들었기 때문에 데이터를 구분해서 생각을 못하고 하나로 관리한다고만 생각했는데, 뷰모델에서는 필터링 된 likedDataList 하나만 사용하고있었기 때문에 SearchFragment의 클릭이랑 호환이 뭔가 이상했던 것이다.
개별관찰도 안되고..
=> 튜터님은 SearchViewModel을 따로 만들어서, dataList도 LiveData로 바로 관찰하자고 해주셨는데, 나는 그냥 sharedViewModel안에 같이 만들어버렸다.
package com.example.myimgsearch
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class SharedViewModel : ViewModel() {
private val _likedDataList = MutableLiveData<Set<KakaoImageData>>(mutableSetOf())
val likedDataList: LiveData<Set<KakaoImageData>> get() = _likedDataList
private val _searchDataList: MutableLiveData<MutableList<KakaoImageData>> = MutableLiveData()
val searchDataList: LiveData<MutableList<KakaoImageData>> get() = _searchDataList
fun addDataList(dataList: MutableList<KakaoImageData>) {
_searchDataList.value = dataList.toMutableList()
}
fun addFavorite(kakaoImageData: KakaoImageData) {
_likedDataList.value = _likedDataList.value?.toMutableSet()?.apply {
add(kakaoImageData)
} ?: mutableSetOf()
Log.d("뷰모델검사", "${_likedDataList.value}")
Log.d("뷰모델사이즈검사", "${_likedDataList.value?.count()}")
}
fun removeFavorite(kakaoImageData: KakaoImageData?) {
_likedDataList.value = _likedDataList.value?.toMutableSet()?.apply {
remove(kakaoImageData)
} ?: mutableSetOf()
Log.d("리무브검사","리무브검사")
}
fun clearLikedDataList() {
_likedDataList.value = emptySet()
}
}
SearchFragment에서 datalist로만 관리하고있던 것을 addDataList함수를 통해 _searchDataList에 담아서 관찰할 수 있도록하고,
addFavorite과 removeFavorite를 통해 선택한 값이 차곡차곡 들어가고 삭제될 수 있도록 만들었다.
clearLikedDataList는 SharedPreference로 저장한 보관함 값을 비울 때,
SharedPreference뿐만 아니라, 보여지는 화면에서도 동일하게 비워질 수 있도록 함수를 만들었다.
[트러블슈팅] _likedDataList값을 MutableLiveData<Set<KakaoImageData>>()로 원래는 선언했었는데, 이렇게 만드니까 첫번째 클릭에는 그냥 []빈값만 생기고, 두번째 값부터 값이 원래대로 들어가는 문제가 발생했었다.
=> 그냥 ()라고만 만들어놓으면 초기값이 null이기 때문에 첫번째 클릭을 .toMutableSet()을 호출하는데 사용한다고 한다. 그래서 (mutableSetOf())으로 빈 리스트를 설정해주는 방법으로 해결했다.
=> SharedViewModel을 이렇게 바꾸고 나니까, remove할 때 클릭한 값 자체를 넘겨서 리스트에서 삭제해주기 때문에, IndexOutOfBoundsException가 발생할 일이 없게 되었다. 또, likedDataList를 Set컬렉션으로 관리하기 때문에, 중복값이 들어가서 보관함이 복잡해지는 경우도 방지할 수 있게 되었다.
...
private fun clickLiked() {
adapter.itemClick = object : SharedListAdapter.ItemClick {
override fun onClick(view: View, position: Int) {
if (!dataList[position].isliked) {
dataList[position].isliked = true
sharedViewModel.addFavorite(dataList[position])
saveFavorite(position)
} else {
dataList[position].isliked = false
sharedViewModel.removeFavorite(dataList[position])
removeFavorite(dataList[position].thumbnailUrl)
}
adapter.notifyItemChanged(position)
}
}
}
private fun saveFavorite(position: Int) {
val pref = requireContext().getSharedPreferences("favorite_prefs", 0)
val editor = pref?.edit()
val likedDataJson = Gson().toJson(dataList[position])
editor?.putString("FavoriteData$position", likedDataJson)
editor?.apply()
}
private fun removeFavorite(thumbnailUrl: String) {
val pref = requireContext().getSharedPreferences("favorite_prefs", 0)
val editor = pref.edit()
val allData: Map<String, *> = pref.all
for ((key, value) in allData) {
if (value is String && value.contains(thumbnailUrl)) {
editor.remove(key)
}
}
editor.apply()
}
...
SearchFragment에서 클릭을 통해 likedData에 저장이 되기 때문에, 여기에 SharedPreference를 저장할 수 있는 함수를 만들었다.
likeDataJson에 GSON을 통해 JSON데이터를 객체로 변환한dataList[postion] 값을 FavoriteData$position키로 저장한다.
저장된 값을 삭제하기 용이하게 하기 위해서 아이템을 하나씩 저장해야하기 떄문에, "$position"값을 활용하여 이름이 각각 들어갈 수 있도록 해줬다. 근데 사실 여기의 position값은 검색으로 가져온 80개의 데이터의 position값이라 클릭할 때마다 position이 겹칠 수가 있다. 사실 이 뒤에 랜덤한 숫자라도 더해야하나 고민을 했는데, 제출 시간이 가까워져서 일단 이렇게 만들고 말았다. 랜덤숫자도 겹칠 가능성이 있긴 하지만,, 나중에 프로젝트가 커지면 그때 겹치지 않는 랜덤함수를 덧붙이든 다른 경우를 만들든 해서 보충하고, 일단은 1부터100까지 숫자중에 랜덤으로 들어가도록 코드를 보완해놓을 것이다.
+생각해보면 FavoriteData라고 카멜케이스로 이름을 만들었는데, 이것도 리소스에 저장하니까 스네이크케이스로 이름을 바꿔줘야겠다..
removeFavorite에서는 키를 찾아서 삭제하는 것이 아니고, for문을 통해 고유한 thumbnailUrl로 값에 해당하는 키를 찾아서 삭제하는 것이기 때문에, 아이템을 중복되지않는 키로 저장할 수 있게 만드는 것이 중요한 것 같다. thumbnailUrl을 String으로 받았기 때문에 if문에서 그냥 value.contains(thumbnailUrl)라고만 적을 수가 없었다. 타입을 맞춰주는 과정에서 value is String && value.contains(thumbnailUrl)이렇게 적게 되었다.
[트러블슈팅] 진짜 별거 아닌 트러블 슈팅인데, removeFavorite에서 처음에 editor.apply()를 빼먹어서 아무리 눌러도 삭제가 안되었다..ㅎ 어떻게 빼먹을 수있지? 어이없기는 한데 어쨌든 얘를 찾는 과정에서 알게된게, apply()로 저장하는 방법은 동기식으로 데이터를 저장하는 것이고, 만약 비동기식으로 저장하고 싶다면 commit()함수를 활용할 수 있다는 것이다. 그래도 UI스레드를 차단하지 않고 데이터를 저장할 수 있기 때문에 보편적으로 권장하는 것은 apply()이고 비교적 더 효율적으로 동작한다고 한다.
...
adapter.itemClick = object : SharedListAdapter.ItemClick {
override fun onClick(view: View, position: Int) {
if (position < adapter.currentList.size) {
adapter.currentList[position].isliked = false
sharedViewModel.removeFavorite(adapter.currentList[position])
removeFavorite(adapter.currentList[position].thumbnailUrl)
}
}
}
binding.btnStorageClear.setOnClickListener {
removeAllFavorite()
sharedViewModel.clearLikedDataList()
}
private fun removeAllFavorite() {
val pref = requireContext().getSharedPreferences("favorite_prefs", 0)
val editor = pref.edit()
editor.clear()
editor.apply()
}
private fun removeFavorite(thumbnailUrl: String) {
val pref = requireContext().getSharedPreferences("favorite_prefs", 0)
val editor = pref.edit()
val allData: Map<String, *> = pref.all
for ((key, value) in allData) {
if (value is String && value.contains(thumbnailUrl)) {
editor.remove(key)
}
}
editor.apply()
}
...
사실 removeFavorite은 StroageFragment에서 먼저 만들었다. 그래서 여기서 만든 함수를 SearchFragment에 공유하는 방식으로 구현이 될까 손을 대봤다가 생각처럼 StrogeFragment().removeFavorite한다고 뚝딱 되는게 아니기도 하고, 그렇다고 adapter나 sharedViewModel에서 사용하는 것은 용도에 맞지 않은 것 같아서,, 제일 간편한 방법인.. 똑같은 함수를 그냥 각 각 사용하도록 복붙해줬다.. ㅎㅎ
removeAllFavorite함수는.. 수요일에.. 밤에.. 급하게 SharedPreference를 저장할려고 만들다 보니까 아이템 각각을 저장하는게 아니고, likedDataList를 통째로 저장하는 방법을 선택했더니, 삭제도 통째로 되어서.. 일단 보관함에 어떻게 저장되는지 확인하려면 데이터가 복잡해지면 안되니까 비워버리는 함수를 하나 만들다가 전체삭제 버튼이 의외로 나쁘지 않은 것 같아서 내비뒀다. clear()함수가 favorite_prefs파일 안에 저장된 값들을 전부 없애준다.
오늘의 문제점이라기엔.. 어제부터.. 어쩌면 그 전 부터의 문제점인데.. StorageFragment에서 클릭해서 아이템을 보관함에서 지우면,, isliked값이 false인건 SearchFragment에도 공유가 되는 것 같은데.. 하트아이콘이 지워지질 않는다. 미치겠다. 사실 튜터님의 권장? 사항을 듣지 않고 SharedViewModel에 searchDataList도 같이 구현한 이유가 이 문제 때문이다.
처음에는. isliked 자체의 값이 안바뀐줄 알고, SharedViewModel에 removeFavorite안에서 searchDataList도 같이 변화를 줄 수 있게 아니면, 새로 updateFavorite함수라도 새로 만들어서 searchDataList의 값도 같이 추적관찰할 수있게 만드려고 갖은 노력을 했다가 더 어지러워지기만 해서 전부다 없애 버리고, 로그도 찍고, 계속 클릭해보고 adapter를 두개의 프래그먼트에서 아예 공유해보게도하고, observe도 이렇게 걸어보고 저렇게 걸어보고 별 짓을 다했는데.. 알아낸게 일단 StorageFragment에서 해당 값을 클릭하면 isliked가 SearchFragment에서도 false로 바뀌는것 같기는 하다는거다. 근데 왜 하트아이콘이 그대로 남아있냐고.. 하트 아이콘은 리사이클러뷰와 관련된 xml이라서 adapter에서만 건드릴 수 있는데.. submitList도 함부로 걸면 오히려 리사이클러뷰 아이템이 싹 다 날라가기나 하고 아직도 원인을 모르겠다.
이 문제점을 쓰면서 깨달은건데.. SharedViewModel에서 likedDataList에서 아이템 삭제할 때도 removeFavorite이고, SharedPreference에 있는 아이템 삭제할 떄도 removeFavorite이름의 함수를 쓴다.. 안그래도 Shared Shared 헷갈려죽겠는데.. 이름도 바꿔줘야겠다..
어쨌든.. 여기서 말한 간단한 문제점들을 고치고.. 해설영상으로 올려준 보완영상을 보면서 하트아이콘 이슈를 고칠 수 있으면 고쳐보고.. 안되면 .. 안되면..... 튜터님께.. 재방문을 해보고.. 사실 해설영상이 내 코드랑 너무 차이가 많이 날까봐 무섭다... 차이가 많이 나면 봐도 내 코드에 적용을 못하니까.ㅎ 이번에 과제를 하면서 본강의 수준별수업강의 온갖 구글링 등등을 보면서 뭐 Repository부터 뭐 로딩중일때 에러날때 다 구분한 코드 등등 별거를 다봤지만.. 이해를 못하면 적용을 못하니까 하나하나 뽑아서 기우느라 고생했다. 그래서 팀프로젝트가 좀 걱정이 되는데.. 여튼 보완할 수 있을만큼 보완하고.. 안되면 되는걸로 어떻게 잘해서 금요일까지 개인과제 마무리하는 걸로 노력해봐야겠다. 파이팅~!