fix(content): 전체 탭 pagination 스크롤을 안정화한다
This commit is contained in:
@@ -411,11 +411,11 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
hideAllTabEmptyError()
|
hideAllTabEmptyError()
|
||||||
updateAllTabGridItemWidth()
|
updateAllTabGridItemWidth()
|
||||||
if (state.selectedType.usesSeriesItems()) {
|
if (state.selectedType.usesSeriesItems()) {
|
||||||
binding.rvContentAllItems.adapter = contentAllSeriesCardAdapter
|
setAllTabAdapterIfNeeded(contentAllSeriesCardAdapter)
|
||||||
contentAllAudioCardAdapter.submitItems(emptyList())
|
contentAllAudioCardAdapter.submitItems(emptyList())
|
||||||
contentAllSeriesCardAdapter.submitItems(state.seriesItems)
|
contentAllSeriesCardAdapter.submitItems(state.seriesItems)
|
||||||
} else {
|
} else {
|
||||||
binding.rvContentAllItems.adapter = contentAllAudioCardAdapter
|
setAllTabAdapterIfNeeded(contentAllAudioCardAdapter)
|
||||||
contentAllSeriesCardAdapter.submitItems(emptyList())
|
contentAllSeriesCardAdapter.submitItems(emptyList())
|
||||||
contentAllAudioCardAdapter.submitItems(state.audioItems)
|
contentAllAudioCardAdapter.submitItems(state.audioItems)
|
||||||
}
|
}
|
||||||
@@ -439,6 +439,11 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
contentAllSeriesCardAdapter.setGridItemWidthPx(widthPx)
|
contentAllSeriesCardAdapter.setGridItemWidthPx(widthPx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setAllTabAdapterIfNeeded(adapter: RecyclerView.Adapter<*>) {
|
||||||
|
if (binding.rvContentAllItems.adapter == adapter) return
|
||||||
|
binding.rvContentAllItems.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
private fun bindAllTabEmpty(state: MainContentAllTabUiState.Empty) {
|
private fun bindAllTabEmpty(state: MainContentAllTabUiState.Empty) {
|
||||||
bindAllTabControls(state)
|
bindAllTabControls(state)
|
||||||
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||||
@@ -485,7 +490,6 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun clearAllTabItems() {
|
private fun clearAllTabItems() {
|
||||||
binding.rvContentAllItems.adapter = null
|
|
||||||
contentAllAudioCardAdapter.submitItems(emptyList())
|
contentAllAudioCardAdapter.submitItems(emptyList())
|
||||||
contentAllSeriesCardAdapter.submitItems(emptyList())
|
contentAllSeriesCardAdapter.submitItems(emptyList())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.main.content.ui
|
|||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.databinding.ItemContentAudioCardBinding
|
import kr.co.vividnext.sodalive.databinding.ItemContentAudioCardBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
@@ -17,12 +18,13 @@ class ContentAllAudioCardAdapter(
|
|||||||
fun setGridItemWidthPx(widthPx: Int) {
|
fun setGridItemWidthPx(widthPx: Int) {
|
||||||
if (widthPx <= 0 || gridItemWidthPx == widthPx) return
|
if (widthPx <= 0 || gridItemWidthPx == widthPx) return
|
||||||
gridItemWidthPx = widthPx
|
gridItemWidthPx = widthPx
|
||||||
notifyDataSetChanged()
|
if (items.isNotEmpty()) notifyItemRangeChanged(0, items.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun submitItems(items: List<MainContentAllAudioUiModel>) {
|
fun submitItems(newItems: List<MainContentAllAudioUiModel>) {
|
||||||
this.items = items
|
val diffResult = DiffUtil.calculateDiff(ContentAllAudioDiffCallback(items, newItems))
|
||||||
notifyDataSetChanged()
|
items = newItems
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
@@ -41,6 +43,20 @@ class ContentAllAudioCardAdapter(
|
|||||||
|
|
||||||
override fun getItemCount(): Int = items.size
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
private class ContentAllAudioDiffCallback(
|
||||||
|
private val oldList: List<MainContentAllAudioUiModel>,
|
||||||
|
private val newList: List<MainContentAllAudioUiModel>
|
||||||
|
) : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize(): Int = oldList.size
|
||||||
|
override fun getNewListSize(): Int = newList.size
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
|
oldList[oldItemPosition].audioContentId == newList[newItemPosition].audioContentId
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
|
oldList[oldItemPosition] == newList[newItemPosition]
|
||||||
|
}
|
||||||
|
|
||||||
class ViewHolder(
|
class ViewHolder(
|
||||||
private val binding: ItemContentAudioCardBinding,
|
private val binding: ItemContentAudioCardBinding,
|
||||||
private val onAudioClick: (Long) -> Unit
|
private val onAudioClick: (Long) -> Unit
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.main.content.ui
|
|||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.databinding.ItemContentAllSeriesCardBinding
|
import kr.co.vividnext.sodalive.databinding.ItemContentAllSeriesCardBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
@@ -17,12 +18,13 @@ class ContentAllSeriesCardAdapter(
|
|||||||
fun setGridItemWidthPx(widthPx: Int) {
|
fun setGridItemWidthPx(widthPx: Int) {
|
||||||
if (widthPx <= 0 || gridItemWidthPx == widthPx) return
|
if (widthPx <= 0 || gridItemWidthPx == widthPx) return
|
||||||
gridItemWidthPx = widthPx
|
gridItemWidthPx = widthPx
|
||||||
notifyDataSetChanged()
|
if (items.isNotEmpty()) notifyItemRangeChanged(0, items.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun submitItems(items: List<MainContentAllSeriesUiModel>) {
|
fun submitItems(newItems: List<MainContentAllSeriesUiModel>) {
|
||||||
this.items = items
|
val diffResult = DiffUtil.calculateDiff(ContentAllSeriesDiffCallback(items, newItems))
|
||||||
notifyDataSetChanged()
|
items = newItems
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
@@ -41,6 +43,20 @@ class ContentAllSeriesCardAdapter(
|
|||||||
|
|
||||||
override fun getItemCount(): Int = items.size
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
private class ContentAllSeriesDiffCallback(
|
||||||
|
private val oldList: List<MainContentAllSeriesUiModel>,
|
||||||
|
private val newList: List<MainContentAllSeriesUiModel>
|
||||||
|
) : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize(): Int = oldList.size
|
||||||
|
override fun getNewListSize(): Int = newList.size
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
|
oldList[oldItemPosition].seriesId == newList[newItemPosition].seriesId
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
|
oldList[oldItemPosition] == newList[newItemPosition]
|
||||||
|
}
|
||||||
|
|
||||||
class ViewHolder(
|
class ViewHolder(
|
||||||
private val binding: ItemContentAllSeriesCardBinding,
|
private val binding: ItemContentAllSeriesCardBinding,
|
||||||
private val onSeriesClick: (Long) -> Unit
|
private val onSeriesClick: (Long) -> Unit
|
||||||
|
|||||||
@@ -572,6 +572,59 @@ class ContentMainFragmentSourceTest {
|
|||||||
assertSourceContains(seriesCard, "172f / 122f")
|
assertSourceContains(seriesCard, "172f / 122f")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `content 전체 탭 pagination 렌더링은 RecyclerView adapter를 보존한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
val bindContentSource = source.substringFrom("private fun bindAllTabContent")
|
||||||
|
val clearItemsSource = source.substringFrom("private fun clearAllTabItems")
|
||||||
|
val destroyViewSource = source.substringFrom("override fun onDestroyView")
|
||||||
|
|
||||||
|
assertSourceContains(source, "private fun setAllTabAdapterIfNeeded(adapter: RecyclerView.Adapter<*>)")
|
||||||
|
assertSourceContains(source, "if (binding.rvContentAllItems.adapter == adapter) return")
|
||||||
|
assertSourceContains(bindContentSource, "setAllTabAdapterIfNeeded(contentAllSeriesCardAdapter)")
|
||||||
|
assertSourceContains(bindContentSource, "setAllTabAdapterIfNeeded(contentAllAudioCardAdapter)")
|
||||||
|
assertFalse(
|
||||||
|
"페이지 추가 상태 재렌더링은 같은 adapter를 직접 재대입하면 스크롤 위치가 흔들릴 수 있다.",
|
||||||
|
bindContentSource.contains("binding.rvContentAllItems.adapter = contentAll")
|
||||||
|
)
|
||||||
|
assertFalse(
|
||||||
|
"Loading/Empty/Error 렌더링의 item clear는 adapter를 분리하지 않아야 한다.",
|
||||||
|
clearItemsSource.contains("binding.rvContentAllItems.adapter = null")
|
||||||
|
)
|
||||||
|
assertSourceContains(destroyViewSource, "binding.rvContentAllItems.adapter = null")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `content 전체 탭 adapter는 DiffUtil로 변경분만 반영한다`() {
|
||||||
|
val audioAdapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllAudioCardAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
val seriesAdapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllSeriesCardAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
val audioSubmitSource = audioAdapter.substringFrom("fun submitItems")
|
||||||
|
val seriesSubmitSource = seriesAdapter.substringFrom("fun submitItems")
|
||||||
|
|
||||||
|
assertSourceContains(audioAdapter, "import androidx.recyclerview.widget.DiffUtil")
|
||||||
|
assertSourceContains(seriesAdapter, "import androidx.recyclerview.widget.DiffUtil")
|
||||||
|
assertSourceContains(audioSubmitSource, "DiffUtil.calculateDiff")
|
||||||
|
assertSourceContains(seriesSubmitSource, "DiffUtil.calculateDiff")
|
||||||
|
assertSourceContains(audioSubmitSource, "diffResult.dispatchUpdatesTo(this)")
|
||||||
|
assertSourceContains(seriesSubmitSource, "diffResult.dispatchUpdatesTo(this)")
|
||||||
|
assertSourceContains(audioAdapter, "oldList[oldItemPosition].audioContentId ==")
|
||||||
|
assertSourceContains(seriesAdapter, "oldList[oldItemPosition].seriesId ==")
|
||||||
|
assertSourceContains(audioAdapter, "oldList[oldItemPosition] == newList[newItemPosition]")
|
||||||
|
assertSourceContains(seriesAdapter, "oldList[oldItemPosition] == newList[newItemPosition]")
|
||||||
|
assertFalse(audioSubmitSource.contains("notifyDataSetChanged()"))
|
||||||
|
assertFalse(seriesSubmitSource.contains("notifyDataSetChanged()"))
|
||||||
|
assertFalse(audioAdapter.contains("notifyDataSetChanged()"))
|
||||||
|
assertFalse(seriesAdapter.contains("notifyDataSetChanged()"))
|
||||||
|
assertSourceContains(audioAdapter, "notifyItemRangeChanged(0, items.size)")
|
||||||
|
assertSourceContains(seriesAdapter, "notifyItemRangeChanged(0, items.size)")
|
||||||
|
}
|
||||||
|
|
||||||
private fun banner(
|
private fun banner(
|
||||||
eventItem: EventItem? = null,
|
eventItem: EventItem? = null,
|
||||||
creatorId: Long? = null,
|
creatorId: Long? = null,
|
||||||
|
|||||||
Reference in New Issue
Block a user