마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

RecyclerView에 하나의 item의 dto가 두가지 있을때의 해결볍

0 추천

안녕하세요 제가 백엔드랑 협업을 하면서  저 게시물의 Response가 따로 이모지의 Response가 따로

각각 받아아 하는 상황 입니다. 저는 adapter에서 저 게시물의 Response로 bind를 하고 이모지는 서버 통신 성공 했을 시

값을 넣어주려고 하는데 값이 제대로 들어가질 않습니니다. 어떡해 해결해야 할까요?

hifl (670 포인트) 님이 2021년 10월 25일 질문
관련된 코드를 보여주시는게 문제를 파악하기가 쉬워보여요.
먼저 시도해보라고 권하고 싶은 방법은 백엔드와 상의해서 게시물 응답에 이모지응답을 합쳐서 줄 수 있는지 물어보시라는 겁니다. 아주 까다로운 작업은 아니므로, 백엔드쪽에서 그렇게 처리해준다면, 제일 좋은데, 그렇게 못한다면, 모바일앱에서는 처리할 일이 상당히 많아집니다.
백엔드 에서 금액적인 이유 때문에 RDB를 사용하지 못한다고 합니다 ㅠㅠ
그렇군요. RDBMS를 사용하지 않더라도 백엔드에서 데이터를 fetch에서 두 데이터를 merge g해줄 수 있다면 속도, 보안, 유지보수의 용이성 등등의 측면에서 많이 이득인데... 아쉽네요.
서버가 데이터를 페이징처리해서 보내준다면, 한번에 작은 양의 데이터만 가져오므로, 데이터를 합치는데 별 문제가 없을 것 같은데...
안된다면, 제일 간단한 방법은  먼저 게시물 데이터를 가져오시고 나서, 백그라운드에서 10개 20개 단위로 게시물의 ID을 가져와서 이모지데이터를 가져오면 방법을 시도해 볼만 할 것 같아요. 이걸 로걸에 해시맵같은 인스턴스를 만드시고 게시물 ID를 키로해서 이모지 데이터를 가져올 때만다 맵에 저장하세요. 다음에 이모지 데이터를 가져와야 할 때는 이 맵을 먼저 체크해서 있으면, 네트워크 호출을 하지 마시구요.
사실, 님이 하시는 형태의 데이터 처리가 현재 제 프로젝트에 가장 빈번히 일어나는 패턴이예요. 훨씬 더 복잡하긴 하지만...

1개의 답변

0 추천
    override fun onBindViewHolder(holder: UserHomeItemViewHolder, position: Int) {
        val item = getItem(position)

        if (item != null) {
            getEmoji(item.number)
            holder.bind(
                item, userViewModel.getEmojiData.value
            )

            if (holder.bindingAdapterPosition != RecyclerView.NO_POSITION) {
                holder.binding.goodEmoji.setOnClickListener {
                    Log.d("TAG", "onBindViewHolder: good")
                    listener?.onGoodItemClick(item, holder.binding.goodEmoji)
                    CoroutineScope(IO).launch {
                        updateEmojiStatus(number = item.number)
                    }
                }
                holder.binding.sadEmoji.setOnClickListener {
                    Log.d("TAG", "onBindViewHolder: sad")
                    listener?.onSadItemClick(item, holder.binding.sadEmoji)
                    CoroutineScope(IO).launch {
                        updateEmojiStatus(number = item.number)
                    }
                }
            }

            insertEmojiStatus(item.number)


            holder.binding.declarationBtn.setOnClickListener {
                val direction: NavDirections =
                    UserMainFragmentDirections.actionUserMainFragmentToDeclarationFragment(item.id)
                it.findNavController().navigate(direction)
            }


        }
    }

    private suspend fun updateEmojiStatus(number: Int) {
        CoroutineScope(IO).launch {
            Log.d(
                "adapter",
                "insertEmojiStatus: update  sad : ${userViewModel.sadStatus.value.toString()} good :${userViewModel.goodStatus.value.toString()}"
            )
            localViewModel.updatePostStatus(
                PostStatus(

                    userViewModel.sadStatus.value,
                    userViewModel.goodStatus.value, number
                )
            )
        }
    }


    private fun insertEmojiStatus(number: Int) {
        CoroutineScope(IO).launch {
            Log.d(
                "adapter",
                "insertEmojiStatus: sad : ${userViewModel.sadStatus.value.toString()} good :${userViewModel.goodStatus.value.toString()}"
            )
            localViewModel.insertPostStatus(
                postStatus = PostStatus(
                    userViewModel.sadStatus.value,
                    userViewModel.goodStatus.value,

                    number
                )
            )
        }
    }


    private fun getEmoji(num: Int) {
        CoroutineScope(IO).launch {
            userViewModel.getEmoji(signInViewModel.token.value.toString(), num)

        }
    }


    class UserHomeItemViewHolder(val binding: UserPostRecyclerItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(data: UserPostDTO, emoji: GetEmoji?) {
            binding.data = data
            binding.emoji = emoji

            binding.executePendingBindings()
        }
    }
hifl (670 포인트) 님이 2021년 10월 25일 답변
ViewModel에서 게시물 + 이모지 데이터를 한번에 받아서 두개를 조합해서 리사이클러뷰 어댑터에 넘겨주세요. ViewHolder안에서 CoroutineScope를 생성하시는 건 핸들링도 힘들지만 좋지 않습니다. 특히 스크롤을 하거나 하면 기존에 실행했던 Coroutine을 취소해주셔 하는 등 처리가 복잡하고 제대로 동작하는지 확인하기 어렵습니다.

ViewModel 에서
viewModelScope.launch {
    val postsResults = getPosts().await()
    val emojisResutl = getEmojis().await()
    
     // 게시물 응답과 이모지 응답결과를 처리 해서 어댑터가 사용할 수 있는 데이터로 만들어 준 다음,
    // 뷰에게 알려 줌.
     val itemsForAdpater = ...
     liveData.postValue(itemsForAdpater)
}

susped fun getPosts() = withContext(Dispatchers,IO) {
      async {
         // 게시물을 가져오는 코드
      }
}

susped fun getEmojis() = withContext(Dispatchers,IO) {
       async {
             // 이모지를 가져오는 코드
        }
}

async + await 를 parallel dispatcher라고 부르는데 비동기식 호출을 여러개를 한꺼번에 처리할 때 사용합니다. 이렇게 하면 최대시간이 걸리는 호출만큼 시간이 걸리며, 모든 응답이 도착할 때까지 대기합니다. 이렇게 비동기 처리시 모든 호출이 완료되는걸 보장할 수 있는데,  이걸 Structured concurrency라고 부릅니다. Coroutine의 강점 중의 하나죠.
ViewModel을 사용하지 않는다면 그냥  viewMoelScope대신에 Activity는 lifecycleScope, Fragment는  viewLifeCycleScope 를 사용하시면 됩니다.
그리고 getPosts와 getEmoji는 exception을 던지지 말고 캐치해서 exception을 감싸서 밖으로 던지셔야 제대로 예외처리가 될 겁니다.

sealed class ApiResult<out T> {
    data class Error(val e: Exception): ApiResult()
    data class Success<T>(val data: T): ApiResult<T>()
}

susped fun getPosts(): ApiResult<PostsType> = withContext(Dispatchers,IO) {
      async {
        try {
             // 게시물을 가져오는 코드
            ApiResult.Success(...)
        } catch (e: Exception) {
           ApiResult.Error(e)
        }
      }
}

susped fun getEmojis() : ApiResult<EmojiType>= withContext(Dispatchers,IO) {
       async {
            try {
                // 이모지를 가져오는 코드
               ApiResult.Success(...)
            } catch (e: Exception) {
                ApiResult.Error(e)
            }
        }
}

ApiResult와 관련해서는 Kotlin 내부에서 사용하는 Result 클래스와 같은 개념의 클래스입니다.  궁금하시면 Kotlin. 소스를 열어서 확인해 보세요.

만약 게시물 API의 응답을 먼저 받아야만 이모지 데이터가 가져올 수 있는 경우라면,
게시물 데이터를 async를 사용하지 말고 viewModel.launch에서 가져온 후,
게시물 데이터는 먼저 어댑터로 보내고 나서
1) 게시물의 갯수가 많지 않다면, 게시물의 id 를 전부  가져와서 이모지 데이터를 한번에 가져오거나
2) 게시물의 개수가 많다면, 먼저 화면에 보일 수 있는 초기 이모지 데이터만 가져온다음,
Recycler 의 LayoutManager의 scroll event 등을 통해 현재 화면에 보여지는 게시물의 이모지만 가져오게 하거나  adapter 의 onAttachedToRecyclerView, onDetachedFromRecyclerView 콜백이 이용가능하다면, 여기에서 이모지데이터를 개별적으로 가져오도록 처리해 줍니다.
2)의 경우는 앱에서 로컬 캐시를 사용하셔야 이미 가져온 이모지 데이터를 다시 가져오지 않게 처리할 수 있습니다.
어렵네요 그래도 대략적인 로직은 이해됐습니다. 감사합니다 모르는거 있으면 또 물어볼게요 ㅎㅎ
...