본문 바로가기

TIL

[TIL] Kotlin Firestore, Storage [ 3 ] 게시글 디테일 페이지로 이동하기, 수정하기 ( 좋아요 기능 )

Topic =  클릭 한 게시글 Item의 디테일 페이지로 이동하기, 수정하기 ( 좋아요 기능 )

 

 


 

 

게시글 디테일 페이지 작업 전 사전 작업

 

지난 글에서 MyPostFeedFragment에서 CurrentUser의 Uid를 통해서 내가 쓴 글을 확인할 수 있도록 했는데, ViewModel을 통해 data를 요청, 받아올 수 있도록 수정.

 

 

게시글 이미지 정보 받아와서 Model 변환 후 Rcv에 올려주기

  • 전에 내가 작성한 글을 확인하는 글에서 Adpater에서 Storage를 호출하는 방식에서 List<Uri>로 변환된 모델을 사용하는 방식으로 변경

 

 

[TIL] Kotlin Storage를 Adapter의 내부에서 호출 시 문제

Topic = Adatper에서 Storage의 이미지에 접근 하는 방식은, Rcv의 스크롤 시 Storage에서 매번 Storage를 통해 이미지를 가져오는 문제가 있다. Rcv에 적용하는 Model의 구조 문제 원인 위와 같이 스크롤 시 이

junes-daily.tistory.com

  • 1. ViewModel 만들기
    • a. Firebase Data 호출 하는 로직 만들기
    • b. Post 형식으로 받아온 데이터를 PostRcv의 형식으로 변환해주기.
    • c. 변환된 데이터를 LiveData에 담아주기
  • 2. Fragment 작업
    • a. Shared ViewModel 적용하기
    • b. Livedata의 변경을 observe 하기
    • c. Livedata의 값을 가져와 Rcv에 뿌려주기
  • 3. Adapter 에서 이미지 로드 방식 변경
    • Storage에 이미지를 요청하지 않고 받아온 Uri로 바로 Image 적용시켜주기

 

위의 작업이 완료 되었으면 디테일 페이지로 이동하는 로직을 작성해보자.

 

디테일 페이지 이동 [ 1 ] ClickListener 만들어주기

 

interface PostClick {
    fun postClick(post: PostRcv)
}
  • 편한 곳에 위와 같이 게시글에 해당하는 Model을 매개변수로 받는 Interface를 생성해준다.

 

class HomePostAdapter(private val postClick: PostClick) :
    ListAdapter<PostRcv, HomePostAdapter.HomePostRcvViewHolder>(DifferCallback.differCallback)
  • Rcv에 해당하는 데이터 Model과 인터페이스에 파라미터의 데이터 형식이 일치해야 한다
  • Adapter Class의 생성자에서 생성해둔 PostClick 인터페이스를 매개변수로 받아 Adapter에서 클릭 이벤트를 처리하기 위한 콜백 인터페이스로 사용할 수 있도록 해준다.

 

    override fun onBindViewHolder(holder: HomePostRcvViewHolder, position: Int) {
        val post = currentList[position]
        
        // 아이템 클릭 리스너 정의
        holder.itemView.setOnClickListener {
            postClick?.postClick(post)
        }
        
        val positionItem = currentList[position]
        holder.apply {
            postCategory.text = "카테고리 : ${positionItem.category}"
            
            ...

        }
        
        // timestamp 에 대해서는 디테일페이지 관련 내용 아래에 추가로 작성할 예정.
        holder.bind(positionItem.imgs[0],positionItem.timestamp)
    }
  • onBindViewHolder 내부에서 클릭 리스너를 정의 해준다.

 

 

디테일 페이지 이동 [ 2 ] Fragment에서 ViewModel로 데이터를 넘기는 클릭 이벤트 정의해주기

 

✔️ Fragment 간의 데이터의 이동을 위해 ViewModel과 LiveData를 통해 데이터를 이동하는 방식은 데이터를 ViewModel에서 관리하여 Fragment 간의 결합도를 낮출 수 있는 방식이다.

 

class MyPostFeedViewModel : ViewModel() {

    ...
    

    // 게시글 목록 Rcv 클릭한 아이템 정보 받아오기
    private val _currentPost = MutableLiveData<PostRcv>()
    val currentPost : MutableLiveData<PostRcv> get() = _currentPost

    // 게시물 디테일 정보 이동하기
    fun setCurrentPost(postRcv : PostRcv){
        _currentPost.value = postRcv
    }
    
    
    ...
    
}
  • PostRcv형식의 값을 파라미터로 받아 해당 데이터를 LiveData에 바로 이동해주는 함수를 정의해준다.

 

    
class HomeFragment() : Fragment() {   
    
    ...
    
    private lateinit var homePostAdapter: HomePostAdapter
    
    private val myPostFeedViewModel: MyPostFeedViewModel by activityViewModels()
    
    override fun onViewCreate(...){
    
        setupRcv()
        
    }
    
    private fun setupRcv() {
        homePostAdapter = HomePostAdapter(object : PostClick {
            override fun postClick(post: PostRcv) {
                myPostFeedViewModel.setCurrentPost(post)

                parentFragmentManager.beginTransaction().add(
                    R.id.frag_edit,
                    PostDetailFragment()
                ).addToBackStack(null).commit()
                Log.d("xxxx", " myPostFeed Item Click = $post ")
            }
        })

        binding.homeRecycle.apply {
            setHasFixedSize(true)
            layoutManager =
                LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
            adapter = homePostAdapter
            addItemDecoration(DividerItemDecoration(context, LinearLayout.VERTICAL))
        }
    }
    
    ...
}
  • ViewModel의 setCurrentPost(post : PostRcv) 의 함수 정의를 한 뒤 HomeFragment에서 위와 같이 ViewModel 함수를 적용해준다
    • 1. Shared ViewModel 적용
      • private val myPostFeedViewModel: MyPostFeedViewModel by activityViewModels()
      • by activityViewModels() 을 통해 별도로 ViewModel Provider을 정의하지 않고 SharedViewModel을 사용할 수 있다.
    • 2. RecyclerView 적용 및 클릭 이벤트 정의
      • ViewModel내부의 serCurrentPost 함수 호출

 

 

 

디테일 페이지 이동 [ 3 ] Detail Fragment에서 Livedata.observe를 통해 데이터 받아오기
class PostDetailFragment : Fragment() {

    ...

    private val myPostFeedViewModel: MyPostFeedViewModel by activityViewModels()

    override fun onCreateView( ... ) {
        _binding = FragmentPostDetailBinding.inflate(inflater, container, false)

        val imgs = mutableListOf<Uri>()
        myPostFeedViewModel.currentPost.observe(viewLifecycleOwner) { it ->
            
            // 받아온 데이터 처리.
            binding.detailId.text = it.nickname
            ...
            
            binding.detailTvLikeCount.text = "${it.likeUsers.size}"

            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
            }
        }

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

        return binding.root
    }
}
  • HomeFragment와 동일한 ViewModel을  SharedViewModel를 통해 DetailFragment에서도 접근할 수 있도록 선언해준 뒤, onCreateView 내부에서 LiveData를 observe 해준다.
  • observe 내부 코드에는 observe를 통해 관찰된 LiveData 값을 처리해주는 로직을 작성해주도록 하자. 

 

 

 

 


 

Firestore DB 게시글 수정하기 [ 좋아요 추가 제거 ]

 

Firebase DB 게시글 수정하기 [ 0 ] Data Modeld 
data class Post(
    val uid: String,
    val title: String,
    ...
    
    val likeUsers: List<String>,
    
    ...
    val timestamp : Timestamp,
    ...
)
{
    constructor() : this("","", "", "", "", "","", listOf(),"", listOf(),"",Timestamp.now(),"")
}
  • 게시글에 좋아요, 관심 목록 추가를 위해서 위와 같이 Data Model 에 likeUsers 를 List<String> 형태로 정의해주고 이후 해당 게시글을 관심 목록에 추가 시 likeUsers List에 좋아요 한 사용자의 uid를 넣어는 방식으로 진행할 것이다.
  • 마이페이지에서 내가 관심 목록에 추가한 글도 쉽게 확인할 수 있게 된다.

 

 

 

Firebase DB 게시글 수정하기 [ 1 ] View 요소 설정하기

 

fragment_detail.xml

    <ImageView
        android:id="@+id/detail_btn_edit_post"
        android:layout_width="80dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/divider"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:padding="12dp"
        android:layout_marginVertical="5dp"
        android:src="@drawable/pencil"
        android:visibility="gone"
        />
  • XML 파일에서 수정 버튼을 정의해준다. 이때, Firebase의 Data 통신이 지연될 경우 게시글 작성자가 아닌 사용자가 수정 버튼 통한 상호작용이 가능하는 문제가 발생할 수 있어 visibility 기본 값을 gone  으로 설정해준다.
    • 해당 방법으로 특정 사용자가 아니면 수정으로 이동할 수 없게 하는 괜찮은 방법은 아닌 것 같다. 추가적인 보안 로직에 대해서도 고민해보면 좋을 것 같다.

 

DetailFragment.kt

    private val currentPostInfo = mutableListOf<PostRcv>()
    
    override fun onCreateView(...) {
    
   
        val imgs = mutableListOf<Uri>()
        myPostFeedViewModel.currentPost.observe(viewLifecycleOwner) { it ->
            binding.detailId.text = it.nickname
            ...
            
            
            binding.detailTvLikeCount.text = "${it.likeUsers.size}"

            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
            }
        }
    }
  • 1. 기존 리스트 일부 내용을 수정할 리스트 객체 생성
    • private val currentPostInfo = mutableListOf<PostRcv>()
    • ViewModel 을 통해 전달받은 currentPost 의 데이터를 수정하고 사용하기 위해 postInfo List를 선언해준 뒤 해당 리스트에 기존 Post의 정보를 추가한다.
  • 2. 현재 currentUser의 Uid와 게시글 작성 시 추가한 작성자 uid를 비교 후 일치하면 수정 버튼이 보이도록 한다.
    • CurrentUser UID - object로 전역변수 설정 vs auth.~ 로 필요할 떄마다 호출하기
      • auth.currentUser!!.uid 를 필요할 때마다 auth 객체를 생성하고 가져오는 것은 호출 시 업데이트 된 최신 정보를 가져오는 장점이 있다. 하지만 앱 시작 초기 Login후 Logout을 별도로 하지 않으면 currentUser.uid는 최신 정보를 가져올 필요가 굳이 없을 뿐더러 이는 대부분의 Fragment에 중복된 코드가 존재하게 된다.
      • object를 통해 currentUser Uid를 앱 초기 로그인 시 전역 변수로 설정하게 되면 한 번의 호출로 전역에서 사용할 수 있기 때문에 호출 코드 중복을 줄일 수 있지만, 사용자 로그아웃, 로그인 등 사용자 정보의 갱신이 필요한 경우 해당 객체의 값을 별도로 업데이트 해주어야 한다.
      • 각각의 장단점이 있는 것 같아 편한 방법으로 사용하면 될 것 같다. 이번 예제 및 프로젝트에서는 작성 편의성 및 가독성을 높이기 위해 object에서 관리하는 방법을 선택했다.
  • 3. 관심 목록 List에 포함 true / false 에 따른 View 옵션 처리
    • 관심 목록에 있는 아이템일 경우 관심 목록에서 제거하기 옵션 버튼이, 관심 목록에 추가되지 않은 아이템일 경우 관심 목록에 추가하기 옵션이 보이도록 설정.
    • 현재 해당 게시글이 이미 관심 목록에 추가한 아이템인지 구별하기 위한 요소 제공

 

 

 

Firebase DB 게시글 수정하기 [ 2 ] 좋아요 추가하기 - 하나의 함수로 처리하기 or 추가,제거 함수 나누어 처리하기

 

[ 2 - 1 ] 하나의 함수로 처리하기 

ViewModel.kt

class MyPostFeedViewModel : ViewModel() {

    ...
    
    // 관심 목록 ( 좋아요 ) 추가 시 카운트 변동
    private val _likeUsersCount = MutableLiveData<MutableList<String>>()
    val likeUsersCount : LiveData<MutableList<String>> get() = _likeUsersCount
    
    // 관심 목록 추가 or 제거
    fun addOrSubFavoritePost(uid: String, postPath: Timestamp){
        db.collection("Posts").whereEqualTo("timestamp", postPath)
            .get()
            .addOnSuccessListener {querySnapshot ->
                if (!querySnapshot.isEmpty) {

                    val documentSnap = querySnapshot.documents[0]
                    val likeList : MutableList<String> = mutableListOf()

                    val writer = documentSnap.data?.get("uid") as String
                    likeList.addAll(documentSnap.data?.get("likeUsers") as List<String>)
                    Log.d("xxxx", " Before control LikeUsers List : $likeList")

                    if (likeList.contains(uid)){
                        // 이미 좋아요한 유저 이벤트 처리
                        likeList.remove(uid)
                        documentSnap.reference.update("likeUsers",likeList)
                            .addOnSuccessListener {
                                Log.d("xxxx", " 관심 목록에서 제거 O ")
                            _likeUsersCount.postValue(likeList)
                            }
                            .addOnFailureListener {
                                Log.d("xxxx", "관심목록에서 제외하기 Failure $it")
                            }
                    } else if(writer == Constants.currentUserUid) {
                        // 작성자 좋아요 예외처리 2
                    } else {
                        likeList.add(uid)
                        documentSnap.reference.update("likeUsers",likeList).addOnSuccessListener {
                            Log.d("xxxx", " 좋아요 버튼 클릭 Successful, 좋아요 List에 UID 추가 " )
                            _likeUsersCount.postValue(likeList)
                            }
                            .addOnFailureListener {
                                Log.d("xxxx", " 좋아요 버튼 클릭 Failure 좋아요 List에 추가 X = $it ")
                            }
                        }
                    }
                }
    }
  • 현재 게시글의 작성된 시간인 timestamp 값을 파라미터로 받아 해당 timestamp와 일치하는 게시글 DB에 접근하여 LikeUsers에 사용자 uid가 포함된 것인지 확인 후 포함되어 있는 경우, 포함되어 있지 않은 경우 각각의 이벤트를 처리한다.
    • 게시글의 기존 LikeUsers에 current User의 uid가 포함되지 않은 상황이면 기존 리스트에 추가하여 업데이트 한다.
    • 이미 currenUser uid가 포함된 상태라면, 기존 리스트에서 제거한 뒤 업데이트 한다.
    • Fragment에서 게시글의 작성자의 좋아요 처리 방지와 ViewModel 에서 실시간으로 DB와 비교하여 처리하는 등 편한 방법으로 작성자의 관심 목록 추가 예외처리를 해주면 될 것 같다.
  • 업데이트가 완료되면 해당 결과를 LiveData에 담아 observe 하고 있는 detailFragment에서 변동된 내용을 확인하여 그에 따른 View업데이트가 이루어질 수 있도록 해주자.

 

DetailFragment.kt

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 관심목록 추가 시 카운트에 반영
        myPostFeedViewModel.likeUsersCount.observe(viewLifecycleOwner){
            binding.detailTvLikeCount.text = "${it.size}"
            
            // 관심 목록에 있는 아이템일 경우, 아닐 경우 처리
            if (it.contains(Constants.currentUserUid)){
                binding.detailBtnSubFavorite.visibility = View.VISIBLE
                binding.detailLike.setImageResource(R.drawable.detail_ic_test_fill_heart)
            } else {
                binding.detailBtnSubFavorite.visibility = View.GONE
                binding.detailLike.setImageResource(R.drawable.detail_ic_border_heart)
            }
        }
        
        // 관심 목록 추가 버튼 클릭 이벤트
        binding.detailBtnAddFavorite.setOnClickListener {
            Util.showDialog(requireContext(),"관심 목록에 추가","내 관심 목록에 추가하시겠습니까?"){
                myPostFeedViewModel.addOrSubFavoritePost(currentPostInfo[0].timestamp)
                Log.d("xxxx", " detail like btn clicked, post timestamp  =  ${currentPostInfo[0].timestamp}")
            }
        }

        // 관심 목록 제거 버튼 클릭 이벤트
        binding.detailBtnSubFavorite.setOnClickListener {
            Util.showDialog(requireContext(),"관심 목록에서 제거","내 관심 목록에서 게시글의 아이템을 제거하시겠습니까?"){
                myPostFeedViewModel.addOrSubFavoritePost(currentPostInfo[0].timestamp)
            }
        }
    }
}
  • Dialog를 통해 관심 목록 추가 및 제거를 처리하는 함수를 실행할 수 있도록 정의해준다.
  • LiveData의 observe 결과로 받은 데이터를 통해 현재 게시글의 좋아요 갯수 및 사용자의 관심 목록 추가 여부를 확인할 수 있도록 View를 업데이트 해준다.
  • Firebase console 및 앱에서 정상적으로 관심 목록에 추가된 것을 확인할 수 있다.

 

 

하나의 함수에서 추가,제거 관리 vs 추가 함수와 제거 함수를 구분하여 각각 함수를 정의하기
   위와 같은 코드 뿐만 아니라 하나의 함수로 여러 이벤트를 처리하는 경우, 단일 함수로 로직을 처리해 간결한(?) 코드 작성이 가능하지만 재사용 성, 유지 보수 및 확장성이 떨어진다.
   하나의 함수는 하나의 이벤트를 처리하는 것은 중복된 코드를 줄이며 유지보수, 확장 및 재사용이 용이한 객체 지향의 원칙인 단일 책임 원칙을 따를 수 있다


ViewModel 에서 AddLikeUser, SubLikeUser를 별도로 분리하여 처리하는 로직을 선택하지 않은 (못한) 이유
1. 프로젝트 초기 별도로 버튼을 구분하지 않고 하나의 버튼으로 작업을 진행하려고 하다보니 자연스럽게 하나의 함수로 두개의 처리를 구분하여 관리하는 방법을 선택하게 되었다.

2. 쿼리문에 대한 학습이 따로 없었던 상황에 해당 기능을 추가하기 위한 키워드를 리스트 값을 변경해서 업데이트를 어떻게 하느냐가 주된 문제로 인식해서 유지보수성 등 디자인의 큰 틀에 대한 신경을 쓰기 어려웠다.

 

[ 2 - 2 ] 관심 목록 추가, 제거 함수 분리하여 정의하기

✔️ 프로젝트에 리팩토링 후 추가 작성할 예정

✔️ 다음 글은 이미지 수정, 카테고리, 게시글 상태 수정 작성 후 링크 추가 예정

 

 


 

 

 

 

 

 


 

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

A. Firebase 데이터 받아오기 복습

 

B. Firestore 쿼리문 학습하기 - 받아올 아이템 갯수 10개 1 - 10 받아오고 11 - 20 받아오는 방법

 

B. 앱 내의 모든 사용자 상호작용이 가능한 기능에 대한 예외처리, 유효성 검사에 대해 자세하게 학습해봐야 겠다.

 

 


 

[오류,에러 등등]

1. 해당 기능 작업 당시에는 오류가 많았는데 따로 정리할 정신이 없었다 

 


 

[느낀 점]

1. 단편적인 기능 구현 뿐만 아니라, 예외처리, 유효성 검사 등 신경쓰자

 

2. 학습하면서 궁금한 내용 적어두고 틈틈이 찾아보기

 

3. 프로젝트 진행하던 것을 조금 지난 뒤 다시 정리하면서 복습하니 도움이 많이 되는 것 같다.

 

 


[Reference]

 

 

// storage

https://firebase.google.com/docs/storage/android/upload-files?hl=ko

// 쿼리

https://firebase.google.com/docs/firestore/query-data/queries?hl=ko#kotlin+ktx_1

// Firebase storage

https://firebase.google.com/docs/reference/kotlin/com/google/firebase/firestore/FirebaseFirestore