TIL

[TIL] Kotlin Firestore, Storage [ 5 ] 게시글 수정한 내용 적용해주기, 단일 LiveData

정상호소인 2023. 10. 31. 23:55

Topic =  게시글 수정 후 원래 디테일 페이지로 이동하는데, 디테일 페이지에 변경된 게시글의 내용 확인할 수 있도록 해주고, 게시글 목록에서도 변경된 내용을 적용해주자.

 

 


 

Logcat이 시강하는 게시글 수정 이전 게시글에 이어서 작성합니다

 

[TIL] Kotlin Firestore, Storage [ 4 ] 게시글 이미지 여러장, 글 수정하기

Topic = 게시글 이미지 여러장 수정하기 [TIL] Kotlin Firestore, Storage [ 3 ] 게시글 디테일 페이지로 이동하기, 수정하기 ( 좋아요 기능 ) Topic = 클릭 한 게시글 Item의 디테일 페이지로 이동하기, 수정하기

junes-daily.tistory.com

 

 

 

 

 

수정된 게시글 적용하기 [ 1 ] 변경된 데이터 넘기기

 

ViewModel.kt

class MyPostFeedViewModel : ViewModel() {


    ...
    
    // 게시글 수정 시 디테일 페이지 데이터 변경
    private val _editPostResult = MutableLiveData<PostRcv>()
    val editPostResult : MutableLiveData<PostRcv> get() = _editPostResult
    
    
    // 게시글 수정 완료 시 디테일 페이지에 데이터 넘겨주기.
    fun setRevisedPost(postRcv : PostRcv){
        _editPostResult.postValue(postRcv)
    }
    
    ...
    
    // Post 형식의 데이터 PostRcv 형식으로 변환
    fun postToPostRcv(post: Post, uriList: MutableList<Uri>): PostRcv {
        return PostRcv(
            post.uid,
            post.title,
            post.price,
            post.category,
            post.address,
            post.deadline,
            post.desc,
            uriList,
            post.nickname,
            post.likeUsers,
            post.token,
            post.timestamp,
            post.state,
            post.documentId
        )
    }
}
  • ViewModel 함수를 통해서 LiveData에 데이터를 넘겨준다.

 

PostEditFragment.kt

class PostEditFragment : Fragment() {

    ...

    private val myPostFeedViewModel: MyPostFeedViewModel by activityViewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    
        binding.btnComplete.setOnClickListener {
            Log.d("xxxx", " postEditFrag 완료 버튼 클릭")

            val post = Post(
                Constants.currentUserUid!!,
                ...
                
               
                ...
                
                currentPost!!.timestamp,
                currentPost!!.state,
                currentPost!!.documentId
            )

            // 디테일 페이지로 수정 된 게시글 정보 이동하기
            myPostFeedViewModel.setRevisedPost(myPostFeedViewModel.postToPostRcv(post,uris))
            
            

 // 수정된 내용 DB 업데이트 - 이전 게시글에서 다룸, 어떤 역할인지 햇갈릴 수 있어서 넣어둠
            myPostFeedViewModel.uploadEditPost(uriAndUrlList,post)
            // 기존 데이터에 있던 Img는 URL 형식으로 분리
            val uriAndUrlList = mutableListOf<Any>()
            ...
            }
            
            parentFragmentManager.popBackStack()
        }
    }
}
  • 게시글 수정 완료 버튼 클릭 시 이전 글에서는 ViewModel을 통해 데이터 업로드만 해놓았는데, 동일한 데이터를 LiveData를 통해서 DetailPage의 View를 변경 해주자.

 

Why - addSnapshotListener를 통해 변경된 정보를 받아오지 않는 이유.


   수정된 내용을 DB에 업로드 시 URI, URL 변환 후 업로드 하는 방식으로 데이터를 업로드 하기 때문에 업로드가 완료되고 변경된 데이터를 다시 Post -> PostRcv 형태로 변환해서 받아와야 하기 때문에 차라리 이미 가지고 있는 데이터로 detail및 home 데이터를 업데이트 하는 것이 좋다고 생각했다.

   현재 100 % MVVM 패턴으로 구현하고 싶어도 할 수 없는 실력이라 내가 작성할 수 있는 선 안에서는 MVVM 패턴으로 리팩토링하게 될 경우 수월하게 할 수 있도록 MVVM 밑 작업을 해보고 있었는데, ViewModel을 통해 데이터를 받아오는 것이 아니라 가공된 데이터를 넘기는 방식이다 보니 해당 코드가 MVVM 패턴과 거리가 멀어 보이는 느낌이 들어서 추가로 확인해보았으나 크게 문제가 되지 않는다고 한다. 프로젝트 진행 전반적으로 일관성 있게 디자인 패턴을 지키지는 못하고 있지만 가능한 선에서는 최대한 지킬 수 있도록 노력해 봐야겠다.

 

 

 

 

 

수정된 게시글 적용하기 [ 2 ] 변경된 데이터 받아오기

 

class PostDetailFragment : Fragment() {
    ...
    
    private val myPostFeedViewModel: MyPostFeedViewModel by activityViewModels()

    private val currentPostInfo = mutableListOf<PostRcv>()

    // View 요소에 해당하는 값으로 데이터 수정하기
    private fun detailPageInfoChange(postRcv: PostRcv) {
        binding.detailId.text = postRcv.nickname
        ...
        
        binding.detailTvLikeCount.text = "${postRcv.likeUsers.size}"
        binding.detailpageTime.text = "${time(postRcv.timestamp)}"
        binding.detailTvItemState.text = postRcv.state
        stateIconChange()
    }
    
    override fun onCreateView(
    
        ...
        
        val imgs = mutableListOf<Uri>()
        myPostFeedViewModel.currentPost.observe(viewLifecycleOwner) {
            detailPageInfoChange(it)
            
            // 게시물 작성자에 따른 이벤트 처리
            if (it.uid == Constants.currentUserUid) {
                binding.detailBtnStateChange.visibility = View.VISIBLE
            }
            
            // 게시물 작성자 프로필 이미지 받아오기
            myPostFeedViewModel.downloadCurrentProfileImg(it.uid)

            // 게시물 수정 페이지 이동 시 넘겨줄 데이터
            currentPostInfo.add(it)
            Log.d("xxxx", " detail Page PostInfo : $currentPostInfo")
            imgs.addAll(it.imgs)

            // 수정 버튼 visibility
            if (it.uid == Constants.currentUserUid) {
                binding.detailBtnEditPost.visibility = View.VISIBLE
            }

            // 관심 목록에 있는 아이템일 경우 binding
            if (it.likeUsers.contains(Constants.currentUserUid)) {
                binding.detailBtnSubFavorite.visibility = View.VISIBLE
                binding.detailLike.setImageResource(R.drawable.detail_ic_test_fill_heart)
            } else {
                binding.detailBtnSubFavorite.visibility = View.GONE
            }
        }

        // 게시물 수정 시 디테일 페이지 View 업데이트
        myPostFeedViewModel.editPostResult.observe(viewLifecycleOwner) { changedPost ->
            if (changedPost.timestamp == currentPostInfo[0].timestamp) {
                detailPageInfoChange(changedPost)

                imgs.clear()
                imgs.addAll(changedPost.imgs)
            }
        }

        // 게시물 작성자 프로필 이미지
        myPostFeedViewModel.currentProfileImg.observe(viewLifecycleOwner) {
            binding.datailProfile.load(it)
        }

        // Viewpager 적용
        val viewPager: ViewPager2 = binding.detailImgViewpager
        val adapter = DetailBannerImgAdapter(imgs)
        viewPager.adapter = adapter

        return binding.root
    }
  • 페이지 불러오기와 관련 없는 코드와 이전 게시글에서 다룬 코드는 제거하려고 했는데 디테일 페이지가 전체적으로 돌아가는 코드를 다시 보면 좋을 것 같아서 제거하지 않았다.
  • 해당 코드에는 현재 DetailFragment 내부에는 currentPost LiveData와 editPostResult LiveData 두개가 존재한다. Fragment내부에서 여러 LiveData를 사용할 때 단일 LiveData로 사용하거나, 여러 LiveData로 나누어서 사용하는 방식이 있는데 이 두가지 방식의 차이에 대해서 간단하게 알아보자.

 

단일 Livedata ?

 

  • 두 개의 LiveData를 사용하는 방식
    • 각각의 데이터가 구분이 되어야 하는 경우에는 LiveData를 분리하여 관리하므 각각의 데이터 변경에 따른 UI 업데이트가 가능하여 명확하게 구분할 수 있다. 하지만 이는 데이터의 중복을 허용하고, observe 패턴의 구현이 복잡해 지는 단점이 존재한다.
  • 단일 LiveData를 사용하는 방식
    • 단일 LiveData를 사용할 경우 하나의 LiveData를 통해 디테일 페이지의 정보를 관리하기 때문에 데이터의 중복을 피하고 코드를 간결하게 유지할 수 있으며 하나의 LiveData만 Observe 하므로 단순하게 구현이 가능하다. 단일 LiveData를 사용하는 경우 초기 디테일 페이지에 접근할 때 Data와 수정 후 의 데이터를 하나의 LiveData로 관리하기 때문에 두 케이스간의 데이터 관리가 복잡해질 수 있다.
  • ✔️각각의 LiveData가 명확하게 분리되어야 하는 상황에는 분리하여 사용하는 것이 좋으나, 데이터 관리 측면에서는 단일 LiveData를 사용하는 것이 바람직할 것 같다.
  • 현재 프로젝트 진행 시에는 기존 포스트 데이터와 게시글 수정 LiveData를 명확하게 구분하여 사용하는 것이 바람직할 것 같다는 생각이 들어 초기 구현 코드에 별도로 수정 X

 

 

단일 LiveData 사용 방법

 

data class 정의

data class PostDetail(
    val currentPost : PostRcv?,
    val editPostResult : PostRcv?
)
  • LiveData에 들어갈 데이터를 data class 를 통해 정의.

 

ViewModel

private val _postDetail = MutableLiveData<PostDetail>()
val postDetail : LiveData<PostDetail> get() = _postDetail

fun setPostDetail(postDetail: PostDeatil) {
    _postDetail.value = postDetail
}
  • ViewModel에서의 함수 정의 해주기.

 

✔️ 두 방식 모두 상황에 따라서 어떤 방식이 더 MVVM 패턴 구현에 적합한지가 다르다고 한다. 중요한 것은 데이터와 UI의 분리를 유지하며, ViewModel을 통해 데이터를 관리하고 UI를 업데이트 하는 것이 우선이므로 해당 원칙을 우선으로 생각하면서 어떤 방식이 MVVM패턴에 더 적합한지 고민해보자.

 

 


 

수정된 게시글 적용하기 [ 3 ] 게시글 목록 RecyclerView 변경된 아이템 수정하기

 

HomeFragment

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    
        ...
        
        // 전체 게시물을 불러오는 LiveData
        myPostFeedViewModel.postResult.observe(viewLifecycleOwner) {

            homePostAdapter.submitList(it)
        }
        
        // 변경된 게시물 정보만 불러오는 LiveData
        myPostFeedViewModel.editPostResult.observe(viewLifecycleOwner){
            val currentPostList = homePostAdapter.currentList.toMutableList()
            for (index in currentPostList.indices){
                if (currentPostList[index].timestamp == it.timestamp){
                    currentPostList[index] = it
                    Log.d("xxxx", " Rcv Item Change = $currentPostList")
                    updateRcv(index,it)
                }
            }
        }
    }

    // 기존 스크롤 포지션을 유지하고 변경된 게시글 반영해주기
    private fun updateRcv(position: Int, post: PostRcv){
        val layoutManager = binding.homeRecycle.layoutManager
        val scrollPosition = if (layoutManager != null){
            (layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
        } else { 0 }
        val newList = homePostAdapter.currentList.toMutableList()
        newList[position] = post
        homePostAdapter.submitList(newList)

        binding.homeRecycle.post{
            binding.homeRecycle.scrollToPosition(scrollPosition)
        }
    }
  • 변경된 데이터를 LiveData를 통해 확인하고, 해당 데이터를 기존 게시글 리스트의 아이템들 중 일치하는 timestamp를 지닌 데이터를 찾아, 변경된 데이터로 변경해준다.

 

 

Adapter

    fun bind(imagePath: Uri, timestamp: Timestamp) {
            postImg.load(imagePath)
            ...

            when (currentList[position].state){
                "교환 가능" -> {
                    postStateBgClosed.visibility = View.INVISIBLE
                    postStateBgPutOff.visibility = View.INVISIBLE
                    postStateBgReservation.visibility = View.INVISIBLE
                }
                "교환 보류" -> postStateBgPutOff.visibility = View.VISIBLE
                "예약 중" -> postStateBgReservation.visibility = View.VISIBLE
                "교환 완료" -> postStateBgClosed.visibility = View.VISIBLE
                else -> return
            }
  • RecyclerView의 아이템에 해당하는 XML에 Frame Layout으로 아이템의 상태를 안내해주는 이미지를 겹쳐 놓고, 해당 아이템의 state 값에 따라 Visibility를 변경하여 사용자가 게시글 상태를 목록에서도 쉽게 확인할 수 있도록 하였다.

 

        fun bind(imagePath: Uri, timestamp: Timestamp) {
        
            ...
            

            val date: Date = timestamp.toDate()
            // 1. 날짜 형식으로 만들기
            // timestamp를 Date 객체로 변환

            // SimpleDateFormat으로 원하는 형식으로 변환
            val dateFormat = SimpleDateFormat("yyyy-MM-dd")

//            postDate.text = dateFormat.format(date)

            // 2. 현재 날짜, 시간 기준으로 만들기 ( 1시간 전, 2일 전, 1달 전)
            val currentDateTime: Date = Date()
            val diff: Long = currentDateTime.time - date.time
            // 분 단위 차이
            val minutes: Long = diff / (1000 * 60)
            val hours: Long = minutes / 60
            val day: Long = hours / 24
            val week: Long = day / 7
            val month: Long = day / 31
            val year: Long = month / 12

            val result: String =
                when {
                    minutes < 1 -> "방금 전"
                    minutes < 60 -> "${minutes}분 전"
                    hours < 24 -> "${hours}시간 전"
                    day in 1..6 -> "${day}일 전"
                    day in 7..13 -> "지난 주"
                    day in 14..30 -> "${week}주 전"
                    month in 1..12 -> "${month}달 전"
                    year in 1..100 -> "${year}년 전"
                    else -> "${dateFormat.format(date)}"
                }
            postDate.text = result

        }
    }
  • 게시글 timestamp 기준으로 현재 날짜와의 차이를 Item에 보여주기 위해서 사용

 

 

rcv_item_thumbnail_img

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ImageView
                android:id="@+id/write_image"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                app:layout_constraintTop_toTopOf="parent"
                app:srcCompat="@drawable/loading2" />

            <ImageView
                android:id="@+id/item_img_state_closed"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/soldout_bg_color"
                android:src="@drawable/bg_state_img_closed_white"
                android:visibility="invisible"
                />

            <ImageView
                android:id="@+id/item_img_state_reservation"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/soldout_bg_color"
                android:src="@drawable/bg_state_img_reservation_white"
                android:visibility="invisible"
                />

            <ImageView
                android:id="@+id/item_img_state_put_off"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/soldout_bg_color"
                android:src="@drawable/bg_state_img_put_off_white"
                android:visibility="invisible"
                />


        </FrameLayout>

 

.목록, 디테일 페이지에 변경된 데이터 반영해주기 끝

 

 

 

 

 

 


 

[ A. 오늘 복습한 내용 / B. 다음에 학습할 내용 ]

A. LiveData 에 대해서 복습 - 단일 LiveData

 

B. 스크롤 정보를 저장하는 방법이 아닌 Rcv의 원래 상태를 유지하면서 변경된 게시글만 변경을 적용하는 방법

 

 


 

[오류,에러 등등]

1. 특별한 오류는 없었다

`


 

[느낀 점]

 

 


[Reference]