onDismissed를 활용한 스와이프 삭제 기능 구현어제 팀원들과 상의해 본 결과, 기능이 통일된 모습이 좀 더 사용성이 좋을 것 같다는 의견이 우세해서 작성한 글도 그냥 되돌리기 스낵바를 통해 삭제 지원을 하기로 했다. 이때, 누르자마자 삭제되고 되돌리기를 누르면 새롭게 아이템이 생성되는 방법이 아니고 onDismissed를 사용해서 스낵바가 사리진 후에 데이터베이스에서 정식으로 삭제되게 구현하는 방식으로 구현하는 것이 좋겠다는 의견을 주셔서 그 방법을 사용하기로 했다.
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
when (recyclerView) {
binding.rvBookmarked -> {
val bookmarkID = bookmarkAdapter.currentList[position]
viewModel.removeBookmarkAdapter(bookmarkID.contentId.toString())
undoSnackbar = Snackbar.make(binding.root, "해당 북마크를 삭제했습니다.", 5000).apply {
anchorView = (activity)?.findViewById(R.id.bottom_navigation)
}
undoSnackbar?.setAction("되돌리기") {
viewModel.undoBookmarkCamp()
if (binding.lineBookmarked.visibility == View.VISIBLE) {
if (position == 0) {
binding.rvBookmarked.post { binding.rvBookmarked.smoothScrollToPosition(0) }
}
} else {
binding.rvBookmarked.visibility = View.GONE
}
}
undoSnackbar?.show()
val snackbarCallBack = object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (event != DISMISS_EVENT_ACTION) {
viewModel.removeBookmarkDB(userId.toString())
}
}
}
undoSnackbar?.addCallback(snackbarCallBack)
}
binding.rvWriting -> {
val postID = postAdapter.currentList[position]
viewModel.removePostAdapter(postID.postId.toString())
undoSnackbar = Snackbar.make(binding.root, "해당 작성 글을 삭제했습니다.", 5000).apply {
anchorView = (activity)?.findViewById(R.id.bottom_navigation)
}
undoSnackbar?.setAction("되돌리기") {
viewModel.undoPost()
if (binding.lineWriting.visibility == View.VISIBLE) {
if (position == 0) {
binding.rvWriting.post { binding.rvWriting.smoothScrollToPosition(0) }
}
} else {
binding.rvWriting.visibility = View.GONE
}
}
undoSnackbar?.show()
val snackbarCallBack = object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (event != DISMISS_EVENT_ACTION) {
viewModel.removePostDB()
}
}
}
undoSnackbar?.addCallback(snackbarCallBack)
}
}
}
onDismissed를 콜백함수로 걸어서 그 안에서 removeBookmarkDB removePostDB를 통해 데이터베이스에서 삭제되도록 로직을 변경했다. 그러나 그냥 onDismissed를 냅다 걸어버리면, 되돌리기를 눌러서 삭제된 아이템을 복구하더라도 스낵바가 사라진 것이기 때문에 데이터베이스에서 삭제되는 현상이 발생했다. 따라서 if (event != DISMISS_EVENT_ACTION)으로 액션버튼(여기서는 되돌리기)를 누르지 않고 스낵바가 사라졌을 때 즉, 시간이 지나서 자동으로 사라졌을 때, 페이지가 바뀌어서 스낵바가 사라졌을 때 등의 상황에서만 데이터베이스에서 삭제되도록 만들었다.따라서 뷰모델에서도 어댑터에서만 사라지게 보이는 함수, 되돌리기 함수, 데이터베이스에서 삭제되는 함수 3가지로 나눠서 작성했다.
private val _bookmarkedList: MutableLiveData<List<CampEntity>> = MutableLiveData()
val bookmarkedList: LiveData<List<CampEntity>> get() = _bookmarkedList
private val _postList: MutableLiveData<List<PostDTO>> = MutableLiveData()
val postList: LiveData<List<PostDTO>> get() = _postList
private var removeBookmarkItem: CampEntity? = null
private var removeBookmarkIndex : Int? = null
private var removePostItem: PostDTO? = null
private var removePostIndex: Int? = null
fun removeBookmarkAdapter(contentID: String) {
_bookmarkedList.value = _bookmarkedList.value?.toMutableList()?.apply {
removeBookmarkItem = find { it.contentId == contentID }
removeBookmarkIndex = indexOf(removeBookmarkItem)
remove(removeBookmarkItem)
} ?: mutableListOf()
}
fun removeBookmarkDB(userID: String) {
val db = FirebaseFirestore.getInstance()
val docRef = db.collection("users").document(userID)
docRef.update("bookmarked",FieldValue.arrayRemove(removeBookmarkItem?.contentId))
}
fun undoBookmarkCamp() {
_bookmarkedList.value = _bookmarkedList.value?.toMutableList()?.apply {
removeBookmarkItem?.let {
if (removeBookmarkIndex != null && removeBookmarkIndex!! in 0 until size) {
add(removeBookmarkIndex!!, it)
} else {
add(it)
}
}
} ?: mutableListOf()
}
fun removePostAdapter(postID: String) {
_postList.value = _postList.value?.toMutableList()?.apply {
removePostItem = find { it.postId == postID }
removePostIndex = indexOf(removePostItem)
remove(removePostItem)
} ?: mutableListOf()
}
fun removePostDB() {
val db = FirebaseFirestore.getInstance()
val docRef = db.collection("posts")
docRef.whereEqualTo("postId", removePostItem?.postId).get().addOnSuccessListener {
for (doc in it) {
doc.reference.delete()
}
}
}
fun undoPost() {
_postList.value = _postList.value?.toMutableList()?.apply {
removePostItem?.let {
if (removePostIndex != null && removePostIndex!! in 0 until size) {
add(removePostIndex!!, it)
} else {
add(it)
}
}
} ?: mutableListOf()
}
removeBookmarkAdapter removePostAdapter를 통해 어댑터와 연결된 라이브 데이터 리스트에서만 해당 아이템이 삭제되도록 했다. 이때, 되돌리기의 경우를 고려하여 삭제한 아이템의 정보(remove~Item)와 위치(remove~Index)도 저장해두었다.undoBookmarkCamp undoPost의 경우에는, 삭제된 아이템의 위치를 기억해서 되돌렸을 때, 삭제했었을 때의 위치로 되돌아갈 수 있도록 만들어주었다.removeBookmarkDB removePostDB는 사용하는 파이어스토어 데이터베이스에서 삭제되도록 저장한 remove~Item에서 일치하는 값을 찾아서 삭제 되도록 만들어주었다.docRef.update 함수를 사용해주었지만, 작성한 글의 경우는 해당 문서(document) 전체를 삭제하는 것이기 때문에 doc.reference.delete()를 사용해주었다.requireContext()에 대한 java.lang.IllegalStateException 오류 수정바텀네비게이션 이동 중에, ProfileFragment에서 사용중인 Glide에 대해서 java.lang.IllegalStateException가 발생했다. 찾아보니까 Glide.with(requireContext()).load(it.getString("profileImage")).into(ivProfileImg)에서 requireContext() 호출에 관련된 문제라고 했다.
=> 처음에는 예외처리를 해줘야한 다는 해결방법을 듣고, try-catch같은 예외 처리를 해줘야한다는줄 알았는데, 글라이드를 사용할 때 그 정도로 예외처리 해주는 방법은 보지 못했고, 기능에 비해 코스트가 과하다는 생각이 들어서 다른 방법을 좀 더 찾아보았다.
=> detach될 때 특히 문제인 것 같아서 Glide를 onDestroyView에서 같이 clear될 수 있도록 Glide.with(requireActivity()).clear(binding.ivProfileImg)을 걸어주었는데, 이래도 똑같은 오류가 발생했다.
=> 결국 그냥 requireContext나 requireActivity를 사용하는 방법 대신 binding.root로 뷰를 참조하도록 바꾸었다.
안드로이드에서 context는 어플리케이션 혹은 액티비티에 대한 포괄적인 정보를 지니고 있는 객체이므로 항상 밀접한 스코프 범위의 context를 골라 사용해야한다는 주의를 듣긴 들었었는데, 이렇게 직접 문제가 발생하니까 까다롭다는 것을 실감하는 것 같다... 이번 프로젝트 이후에 context에 대해서도 좀 더 명확한 정리를 해둘 필요를 느꼈다.
오늘은 스와이프 기능을 보완해서 PR을 완료했다. 이전에 단순하게 전화 걸기 인텐트 연결 이 정도로 사용하는 것만 봤었어서 이렇게 삭제 기능을 만들 때 저런 callback 함수들이 있는 줄 몰랐다. 하다보면 늘 이렇게 경험이 부족한게 느껴진다. 그래도 알려주신 것을 바탕으로 쉽게 만들 수 있는 기능이어서 바로 마무리할 수 있었다. 다행이다.
오늘 예상치 못한 오류를 만나서 시간을 좀 썼다. 위에도 작성했지만, 얼마 전에 받은 예상 기술 면접 질문에서 context관한 이야기가 없었다면, 어떻게 처리해야할지 감도 아예 못올뻔했다. 다만, 그 때 정리한다고 했는데도 부족했던 것 같아서 이번 프로젝트가 끝나면 꼭 한 번 제대로 봐야겠다. 요즘 코드 마감하느라 시간이 없다는 핑계로 자꾸 하나씩 미뤄두는게 생기는 것 같아서 아쉽다.. 근데 실제로 오늘 이미지 저장 권한 코드는 작성 시작도 못해서..ㅎㅎ
또, 오늘 회의 시간과 별개로 마감을 위한 중간 MVP 점검을 했는데, 아직 구현되지 못한 채팅과 연관된 유저디테일정보페이지를 만드는 것 보다는 다른 팀원의 스코프로 있던 구글 로그인 구현 기능이 더 중요하다고 판단되어서, 프로필 화면이 일단락된 내가 빠르게 구글 로그인을 진행하는 것이 좋겠다는 판단이 있었다. 나도 충분히 동의하고 원래 스코프를 가지고 있던 팀원 분도 코드 마감을 위해 아직 마감 못한 구현 중 기능을 정리하고 싶다고 하셔서 내가 빠르게 진행하기로 했다.
따라서 내일은 꼭! 이미지 저장 권한에 관련된 로직을 완성하고, 구글 로그인을 연결해야 겠다. 다음주에는 최종 발표 준비를 하느라 비공개 테스트를 완료하고 진짜 배포를 하려면 금요일에는 꼭 마무리가 되어야한다고 했다. 마감을 지키기 위해 더 파이팅해야겠다.!