본문 바로가기

TIL

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

Topic = Adatper에서 Storage의 이미지에 접근 하는 방식은, Rcv의 스크롤 시 Storage에서 매번 Storage를 통해 이미지를 가져오는 문제가 있다.

 

 


 

Rcv에 적용하는 Model의 구조 문제

 

 

원인

  • 위와 같이 스크롤 시 이미지를 다시 불러와야 하기 때문에 storage에 매번 요청을 보내고 받아온 뒤, 이미지가 반영이 되어 1000 msec 이상의 차이가 나는 문제가 발생.
  • 기존에 Rcv에 뿌려주는 Data의 Model의 imgs 에 해당하는 이미지 목록이 Storage와 상호작용을 위해 List<String> 으로 데이터를 저장했었는데 List의 각 String을 Adapter 내부 bind 에서 처리하기 때문에 Model 구조 자체가 문제로 보여짐.

 

 

문제 해결 방법

  • 게시글을 생성할 때, 게시글을 불러올 때는 Storage의 path 정보를 받아와야 하기 때문에 List<String> 을 바꿀 수는 없다.
    • Firestore DB에 Field 값에는 Uri, Url 등의 형식이 들어가지 않는다
  • 그렇다면, ViewModel 안에서 기존 Post의 Data 형식으로 DB 데이터를 받아온 뒤 List<String> 인 Post 모델이 아닌 List<Uri> 의 형식의 imgs 를 지닌 PostRcv 모델을 만들어서 imgs를 제외한 값에 받아온 Post 를 넣어주고 ViewModel에서 Storage에 이미지를 받아와 PostRcv의 imgs에 Uri List를 넣어주면 해결이 될 것이다.
  • 이방법 또한 초기에 게시글 리스트를 받아올 때 지연되는 시간이 늘어나는 것이기 때문에 근본적인 해결 방법은 아닌 것 같다. 더 좋은 방법을 학습하게 되면 추가 예정

 

해결코드

  • 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 적용시켜주기

 

 

ViewModel.kt
class MyPostFeedViewModel : ViewModel() {
    private val db = Firebase.firestore
    private val storage = Firebase.storage
    
    // Home Frag 게시글 정보 LiveData
    private val _postResult = MutableLiveData<MutableList<PostRcv>>()
    val postResult: LiveData<MutableList<PostRcv>> get() = _postResult

    
    // Home Frag 게시글 정보 받아오기
    fun downloadHomePostRcv(){
        viewModelScope.launch(Dispatchers.IO) {
            try {
                db.collection("Posts")
                    .orderBy("timestamp",Query.Direction.DESCENDING)
                    .get()
                    .addOnSuccessListener { querySnapshot ->
                        if (!querySnapshot.isEmpty) {
                            val postRcvList = mutableListOf<PostRcv>()
                            for ( document in querySnapshot.documents){
                                document.toObject<Post>()?.let { post ->
                                    convertPostToPostRcv(post,querySnapshot, postRcvList,_postResult)
                                }
                            }
                        }
                    }

            }catch (e: Exception){

            }
        }
    }

    // Model 형식 Post  -> PostRcv 형식으로 변환
    private fun convertPostToPostRcv (post: Post, querySnapshot: QuerySnapshot, postRcvList:MutableList<PostRcv>,
                                      resultLiveData: MutableLiveData<MutableList<PostRcv>>) {
        val postImgUris: List<String> = post.imgs
        val postImgList: MutableList<Uri> = mutableListOf()

        val downloadTasks = mutableListOf<Task<Uri>>()
        for (uri in postImgUris) {
            val downloadTask = storage.reference.child("post").child(uri).downloadUrl
            downloadTasks.add(downloadTask)
        }

        // 이미지를 모두 받아온 뒤 한번에 PostRcv의 List<Uri> 에 담아준다 ->
        Tasks.whenAllSuccess<Uri>(downloadTasks)
            .addOnSuccessListener { uriList ->
                postImgList.addAll(uriList)

                if (postImgUris.size == postImgList.size) {
                    val postRcv = PostRcv(
                        uid = post.uid,
                        title = post.title,
                        price = post.price,
                        category = post.category,
                        address = post.address,
                        deadline = post.deadline,
                        desc = post.desc,
                        imgs = postImgList,
                        nickname = post.nickname,
                        likeUsers = post.likeUsers,
                        token = post.token,
                        timestamp = post.timestamp,
                        state = post.state
                    )
                    
                    // 정렬이 깨지는 경우가 있어 timestamp기준 정렬을 한번 더 해준다
                    var inserted = false
                    for ( index in postRcvList.indices){
                        if (postRcv.timestamp > postRcvList[index].timestamp){
                            postRcvList.add(index, postRcv)
                            inserted = true
                            break
                        }
                    }
                    if (!inserted){
                        postRcvList.add(postRcv)
                    }
                    
                    // 결과 Date Livedata에 추가해주기
                    if (postRcvList.size == querySnapshot.size()) {
                        resultLiveData.postValue(postRcvList)
                    }
                }
            }
    }
  • querySnapshot을 파라미터로 받는 이유
    • DB를 통해 받은 모든 데이터가 변환이 끝나면 LiveData에 추가 해주기 위해
  • 빈 postRcvList를 파라미터로 받는 이유
    • 첫번째 데이터의 경우 상관 없지만 2번째, 3번째 데이터가 추가되는 경우 for 문 안에서 이전 데이터를 감지하기 위한 별도의 로직을 추가하기 보다, List를 파라미터로 받아서 이미 추가된 데이터와 timeStamp를 비교해서 바로바로 사용할 수 있도록 하기 위해 파라미터로 받는다.
  • LiveData를 파라미터로 받는 이유
    • 전체 게시물을 받아올 때와 내가 쓴 글 게시물 받아올 때 같은 covertPostToPostRcv 함수를 사용할 것이기 때문에 각각 다른 LiveData에 들어갈 수 있도록 지정하기 위해

 

HomeFragment
class HomeFragment : Fragment() {
    private lateinit var homePostAdapter: HomePostAdapter
    
    // shared ViewModel객체 생성
    private val myPostFeedViewModel: MyPostFeedViewModel by activityViewModels()
        
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 리스트 새로고침
        binding.homeBtnRefreshList.setOnClickListener {
            myPostFeedViewModel.downloadHomePostRcv()
        }

        setupRcv()
        
        // 게시글 리스트 요청하기
        myPostFeedViewModel.downloadHomePostRcv()
        
        // 게시글 리스트 받아오기
        myPostFeedViewModel.postResult.observe(viewLifecycleOwner) {

            Log.d("xxxx", " Home Frag Observe ")
            homePostAdapter.submitList(it)
            homePostAdapter.notifyDataSetChanged()
        }
    }
    
    private fun setupRcv() {
            homePostAdapter = HomePostAdapter()

            binding.homeRecycle.apply {
                setHasFixedSize(true)
                layoutManager =
                    LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
                adapter = homePostAdapter
                addItemDecoration(DividerItemDecoration(context, LinearLayout.VERTICAL))
            }
        }
    }

 

 

 

Adapter
    inner class HomePostRcvViewHolder(binding: WriteItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        val postTitle: TextView = binding.writeTittle
        val postDesc: TextView = binding.writeSubtittle
        val postPrice: TextView = binding.writePrice
        val postCategory: TextView = binding.writeCategory
        val postImg: ImageView = binding.writeImage
        val postheart:ImageView = binding.btnHeart
        val postDate : TextView = binding.writePageDate


        fun bind(image : Uri) {
            postImg.load(imagePath)
       }
   }
  • Viewholder의 bind 내부에서는 coil을 통해 이미지를 view에 적용시켜주자.

 

    override fun onBindViewHolder(holder: HomePostRcvViewHolder, position: Int) {
        val post = currentList[position]

        val positionItem = currentList[position]
        holder.apply {
            postCategory.text = "카테고리 : ${positionItem.category}"
            postTitle.text = positionItem.title
            postDesc.text = positionItem.desc
            postPrice.text = positionItem.price

        }

        holder.bind(positionItem.imgs[0])
    }
  • onBindViewHolder에서는 ViewHolder의 bind 메서드에 현재 아이템의 대표 이미지 첫번째 사진을 파라미터로 지정해 호출해주면 된다.

 

 

역 직렬화

 

역 직렬화

  • 역 직렬화란 디스크, 네트워크 통신 등으로 받은 데이터를 메모리에서 쓸 수 있도록 변환하는 것.
  • Firebase store 역직렬화는 Firestore에 저장된 Document 단위의 데이터 내부에 포함된 Field 를 Kotlin 에서 사용할 수 있도록 변환하는 작업이다.
  • Firebase data는 Firebase SDK의 toObject()<Model> 를 통해 간단하게 역직렬화를 할 수 있다.
db.collection("Posts")
    .orderBy("timestamp",Query.Direction.DESCENDING)
    .get()
    .addOnSuccessListener { querySnapshot ->
        if (!querySnapshot.isEmpty) {
           val postRcvList = mutableListOf<PostRcv>()
           for ( document in querySnapshot.documents){
              document.toObject<Post>()?.let { post ->
              // 각각 post 객체 접근 가능
              }
         }
     }
}

 

 

역 직렬화 요건

data class Post(
    val uid: String,
    val title: String,
    val price: String,
    val category: String,
    val address: String,
    val deadline: String,
    val desc: String,
    var imgs: List<String>,
    val nickname: String,
    val likeUsers: List<String>,
    val token: String,
    val timestamp : Timestamp,
    val state : String,
)

// 생성자 추가
{
    constructor() : this("","", "", "", "", "","", listOf(),"", listOf(),"",Timestamp.now(),"")
}
  • 역직렬화 시 Firestore에서 데이터를 가져올 때 Firestore DB에 해당 필드가 존재하지 않는 경우에도 객체를 정상적으로 생성하는 등 안정성을 보장하기 위해 Model에 초기값을 지정해주어야 한다.
  • 이전에 Model을 위 코드와 같이 초기값을 지정( 초기화 ) 하지 않은 형식으로 정의해두었다면, 해당 data class에 생성자를 추가해줌으로써 정상적으로 역직렬화를 진행할 수 있다.
  • var title : String? = "" 의 형식으로 Model 작성 또는 생성자를 추가하지 않고 toObject()를 통해 역직렬화를 시도할 경우 오류가 발생할 수 있다.

 

변경 전 코드

  • 아래와 같이 Model에 일일이 값을 넣어줄 필요가 없어졌다.
Post(
    document.data?.get("uid") as String,
    document.data?.get("title") as String,
    document.data?.get("price") as String,
    document.data?.get("category") as String,
    document.data?.get("address") as String,
    document.data?.get("deadline") as String,
    document.data?.get("desc") as String,
    document.data?.get("imgs") as List<String>,
    document.data?.get("nickname") as String,
    document.data?.get("likeUsers") as List<String>,
    document.data?.get("token") as String,
    document.data?.get("timestamp") as Timestamp,
    document.data?.get("desc") as String,
    document.data?.get("state") as String,
    
    )

 

 

역 직렬화, 직렬화에 대한 추가적인 정보는 아래 블로그, 문서 등을 학습

 

[Java] 직렬화와 역직렬화

java-study에서 스터디를 진행하고 있습니다. 데이터 직렬화와 역직렬화 데이터 직렬화 메모리를 디스크에 저장하거나, 네트워크 통신에 사용하기 위한 형식으로 변환하는 것이다. 데이터 역직렬

steady-coding.tistory.com

 

 

 

 

 

 


 

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

A. 주중에 프로젝트 진행 중 학습한 내용을 오늘 TIL에 적으면서 해당하는 내용을 다시 복습.

 

B. DB구조를 어떻게 하면 잘 만들 수 있을까...

 

B. 위와 동일한 내용이긴 하지만 DB 데이터 저장, 불러오기에 드는 시간을 짧게 줄이는 방법도 찾아보고 싶다.

 


 

[오류,에러 등등]

1. Adapter에서 bind 시 로딩이 좀 걸리는 문제를 해결하기 위해 Rcv에 이미지 캐싱을 시도 해보았는데 DifferUtil과 캐싱을 동시에 사용하면서 이미지가 다른 이미지로 바뀌지 않는 현상이 종종 생겨서 캐싱 사용 X

 


 

[느낀 점]

1. 많은 사람들이 찾는 블로그는 아니지만, 잘못된 정보를 나만 볼 수 있는 글이 아닌 공개적인 글에 적어서 남들에게 정확하지 못한 정보를 전달할 수 있으니 항상 작성하는 글의 정보가 맞는 정보인지 신경 써서 글을 적도록 해야겠다.

 

2. 프로젝트에 집중하느라 TIL을 쓸 시간이 없다는 못했다는 핑계로 TIL을 며칠 안썼는데, 프로젝트 진행 도중에 느꼈던 어려움, 오류 등을 기록하지 못한 것이 아쉽다.

 

3. 꾸준히 학습하고, 학습을 위한 도구로 TIL을 사용하되 스트레스를 받지 않을 정도로만 활용해보자. 

 

 


[Reference]