๋ชฉ์ฐจ
๋ค์ด๊ฐ๊ธฐ ์ ์
์ํ๋ฅผ ๊ฒ์ํ ํ ์ํ ํฌ์คํฐ๋ค์ ํ๋ฉด์ ๋์ธ ๋ "๋ฌดํ ์คํฌ๋กค " ์ ๊ตฌํํด์ผํ๋ค.
๋ฌดํ ์คํฌ๋กค์ ์์ ๊ตฌํํ๋ฉด ๊ฒช์ ๋ด์ฉ์ ๋ํด ์ ๋ฆฌํด๋ณด๋ คํ๋ค.
๋ฌดํ ์คํฌ๋กค์ ๋ด์ฉ์ ๋จผ์ ์ ๋ฆฌํด๋ณด์
๋ฌดํ ์คํฌ๋กค์ด๋?
๋ฌดํ ์คํฌ๋กค์ด๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํ๋ฒ์ ๋ก๋ํ๋ ๊ฒ์ด ์๋๋ผ ์ฌ์ฉ์๊ฐ ์๋๋ก ์คํฌ๋กคํ ๋ ์ฝํ ์ธ ๊ฐ ๋์ ์ผ๋ก ๋ก๋๋๋ ๊ฒ์ ๋งํ๋ค.
๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํ๋ฒ์ ๊ฐ์ ธ์ค๋ ๊ฒ์ด ์๋๊ธฐ ๋๋ฌธ์ ๋ก๋ฉ ์๊ฐ์ด ์ค์ด๋ ๋ค๋ ์ฅ์ ์ด ์๋ค!
๊ฒ์์ด์ ๋ฐ๋ผ์ ํด๋นํ๋ ์ํ ๋ชฉ๋ก์ page๋ณ๋ก ๊ฐ์ ธ์ค๋๋ก ์ค๊ณ๋ ์๋ฒ api๋ฅผ ์ด์ฉํด์ ๊ตฌํํ๊ธฐ ์ํด page๋ฅผ ์ด์ฉํ ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํด๋ณด์๋ค.
๋ฐ๋ก ๊ฒฐ๊ณผ๋ถํฐ ๋ณด์ฅ
๊ตฌํ๋ ๋ฌดํ ์คํฌ๋กค์ด๋ค !!
๋ฌดํ ์คํฌ๋กค ํ๋จ
์๋ ๋ก์ง์ ๋ฐ๋ผ ์คํฌ๋กค์ด ์ตํ๋จ์ ๋๋ฌํ์์ ํ๋จํด์ ๋ค์ ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์๋ค.
๋ก๋ฉ์ด ๋๊ณ ์๋๊ฑด๊ฐ?!
๊ทธ๋ฐ๋ฐ ํ์ด์ง๊ฐ ๋์ด๊ฐ ๋๋ง๋ค ๋ก๋ฉ์ด ๋๊ณ ์์์ ํ์ธํ ์ ์์ผ๋ฉด ์ข๊ฒ ๋ค.
๊ทธ๋ ๊ฒ ... ๋ก๋ฉ ๋ ์ด์์์ ์ถ๊ฐํด์ฃผ๋ ค๊ณ ํ๋๋ฐ ... ์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ ์๊ฒผ๋ค.!!
๋ฌธ์ ๋ฐ์
๊ทธ๋ฆฌ๋๋ทฐ๋ฅผ ํตํด ์ํ ํฌ์คํฐ๊ฐ ๋จ๋ค๊ฐ ๋ค์ ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ๋๊น์ง ๋ก๋ฉ์ ํ์ํ๊ณ ์ ํ๋ค.
์ฒซ ๋ฒ์งธ ์๋ : ๋จ์ํ ๋ ์ด์์ ํ์ผ์ Lottie์ ๋ก๋ฉ ์ด๋ฏธ์ง๋ฅผ ์ถ๊ฐํด์ฃผ์๋ค.
๊ฒฐ๊ณผ๋ ์๋์ ๊ฐ์๋ค...
๋ก๋ฉ์ด ์๋์ง๋ ํ์ธํ๊ธฐ ์ด๋ ค์ ๊ณ ์ ๋๋ก ๊ตฌํ๋์ง ์์๋ค.
๋ ๋ฒ์งธ ์๋๋ BaseAdapter๋ฅผ ์์๋ฐ์ ํด๋์ค์์ ์ฌ๋ฌ ๋ทฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ด๋ค.
๋ก๋ฉ ์ฌ๋ถ ์ ๋ณด๋ฅผ ํ๋๊ทธ๋จผํธ์์ ์ด๋ํฐ๋ก ์ ๋ฌํด์ฃผ๊ณ ์ฌ๋ถ์ ๋ฐ๋ผ ๋ทฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ฐฉ์์ผ๋ก ์๋ํด๋ณด์๋ค.
๋ง์ง๋ง์ผ๋ก ์ด๋ํฐ์์ ๋ก๋ฉ ์ฌ๋ถ ์ ๋ณด๋ฅผ ๋ฐ๋ ๋ฉ์๋๋ฅผ ์์ฑํด์ฃผ๊ณ ํด๋น ๊ฐ์ ๋ฐ๋ผ์ ๋ทฐ๋ฅผ ๋ฌ๋ฆฌํด์ฃผ์๋ค.
์๋ ์ฝ๋๋ฅผ ๋ณด์.
์ตํ๋จ์ด๋ผ๋ฉด ๋ก๋ฉ์ true๋ก ์ค์ ํด์ฃผ๊ณ , page๋ฅผ 1๋งํผ ์ฆ๊ฐ์ํค ํ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ํธ์ถํด์ฃผ์๋ค.
binding.movieGridView.setOnScrollListener(
object : AbsListView.OnScrollListener {
private var isLoading = false
override fun onScrollStateChanged(
view: AbsListView?,
scrollState: Int,
) {
}
override fun onScroll(
view: AbsListView?,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int,
) {
// !๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ.canScrollVertically(1) : ๊ทธ๋ฆฌ๋๋ทฐ๊ฐ ์ตํ๋จ์ ๋ฟ์ ๊ฒ
// ์ด๋ฅผ ํตํด ์ตํ๋จ์ ๋ฟ์๊ณ ๋ก๋ฉ์ด ์๋๋ผ๋ฉด ๋ก๋ฉ์ค์ผ๋ก ์ํ๋ฅผ ๋ณ๊ฒฝํด์ฃผ๊ณ
// ํ์ด์ง๋ฅผ 1 ์ฆ๊ฐ์ํจ ํ ๋ทฐ๋ชจ๋ธ์ ํตํด ๋ค์ ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋๋ก ํ์๋ค.
if (!binding.movieGridView.canScrollVertically(1) && !isLoading) {
isLoading = true
currentPage++
viewModel.handleEvent(MovieSelectEvent.LoadNextPageMovie(queryText, currentPage))
} else {
isLoading = false
}
}
๊ทธ๋ฆฌ๊ณ StateFlow๋ณ์ moviePosterUriList๊ฐ ๋ค์ ํ์ด์ง๋ฅผ ๊ฐ์ ธ์์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๊ฒ ๋๊ณ
์ด ๋ฐ์ดํฐ๋ฅผ ์ด๋ํฐ์ ๋๊ฒจ์ฃผ์๋ค.
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.moviePosterUriList.collect {
moviePosterAdapter?.initializePosterUriList()
moviePosterAdapter?.setPosterUriList(it)
๊ทธ ๊ฒฐ๊ณผ ... ! ์ํ ํฌ์คํฐ๋ฅผ ๋ฌดํ ์คํฌ๋กค๋ก ๊ฐ์ ธ์ค๋๋ฐ๋ ์ฑ๊ณตํ์์ง๋ง ๋ก๋ฉ์ ํ์ํ๋๋ฐ์๋ ์คํจํ๋ค.
๋ก๋ฉ ํ๋ฉด ์ถ๊ฐ (๊ทธ๋ฆฌ๋๋ทฐ์์ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ๋ก ๊ต์ฒด!?)
์ง๊ธ๊น์ง GridView์ ๊ธฐ๋ณธ ๋ฒ ์ด์ค์ด๋ํฐ๋ก ๊ตฌํํ ์ฝ๋๋ก ์ํ ํฌ์คํฐ๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ์์๋๋ฐ ์๋ฌด๋ฆฌ ํด๋ณด์๋ ๋ฐ์ดํฐ + ๋ก๋ฉ ์ ํจ๊ป ํ์ํ๊ธฐ ์ด๋ ค์ ๋ค.
๐ฑ ๋ก๋ฉ์ง์ฅ์ ๋น ์ง๊ธฐ๋ ํ๋ค ..
๋ฐ๋ผ์ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ๋ฅผ ํตํด ์ํฉ์ ๋ฐ๋ผ ๋ค๋ฅธ ๋ทฐ๋ฅผ ๋ณด์ฌ์ฃผ๋๋ก ํ๊ธฐ ์ํด ๋ณ๊ฒฝํด์ฃผ์๋ค. ๋ฉํฐ๋ทฐ ๋ฐฉ์์ผ๋ก ๊ตฌํํด๋ณธ ๊ฒฝํ์ด ์์ด์ ์ด๊ฑธ๋ก ๊ตฌํํด๋ณด๋ฉด ๋๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์ด ๋ฐ๋ก ๋ณ๊ฒฝํด๋ณด์๋ค!!
// ๊ธฐ์กด
class MoviePosterAdapter(private val context: Context) : BaseAdapter()
// ๋ณ๊ฒฝ
class MoviePosterAdapter(private val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>()
// ๋ก๋ฉ ์ํ ๋ณ์
private var isLoading = false
companion object {
const val VIEW_TYPE_ITEM = 0
const val VIEW_TYPE_LOADING = 1
}
// ๋ก๋ฉ ์ํ ์
๋ฐ์ดํธ๋ฅผ ์ํ ๋ฉ์๋
fun setLoading(isLoading: Boolean) {
this.isLoading = isLoading
}
// ๋ทฐ ํ์
์ ๋ฐ๋ผ ๋ค๋ฅธ ๋ ์ด์์๊ณผ ๋ทฐํ๋๋ฅผ ๋ฐํํด์ฃผ์๋ค.
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_ITEM -> {
val binding = MoviePosterItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
MoviePosterViewHolder(binding)
}
VIEW_TYPE_LOADING -> {
val binding = ItemLoadingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
MovieLoadingViewHolder(binding)
}
โจ์๋๋ก์ด๋ ๊ฐ๋ฐ์ ์ง๋ฌธ๋ฐฉ์ ํตํด ์ป์ ์์คํ ๋ต๋ณ๋ค
์๋๋ก์ด๋ ๊ฐ๋ฐ์ ์ง๋ฌธ๋ฐฉ์ ํตํด ์ง๋ฌธ์ ํ ๊ฒฐ๊ณผ ์๋ 2๊ฐ์ง๋ฅผ ๋ฐฐ์ธ ์ ์์๋ค.
1 . ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ์ footer & header๋ผ๋ ๊ฐ๋ ๊ณผ
2. span ์ฒ๋ฆฌ ๋ฐฉ์์ ๋ํด ์๊ฒ๋์๋ค.
๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ์ footer
๋จผ์ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ์ footer์ ๋ํด์ ์์๋ณธ ๊ฒฐ๊ณผ, ์ด๋ํฐ๋ฅผ ์กฐ์ ํด์ ๋ฐ๋ฅ๊ธ์ ์ถ๊ฐํด์ฃผ๋ ๊ฐ๋ ์ด์๋ค.
๋ด์ฉ์ ๋ณด๊ธฐ ์ ์๋ footer๋ผ๋ ๊ฐ๋ ์ด ๋ฐ๋ก ์กด์ฌํ๋ ๊ฑฐ๋ผ๊ณ ์๊ฐํ์๋๋ฐ ๋ฉํฐ๋ทฐ๋ฅผ ํตํด ๊ตฌํํด์ฃผ๋ ๋ฐฉ์์์ ์๊ฒ๋์๋ค.
ํฌ๊ฒ ์๋์ ํ๋ฆ๋๋ก ๊ตฌํํด์ฃผ๋ฉด ๋๋ ๊ฒ ๊ฐ๋ค. ์ด๋ฏธ ์ ์ฉํด์ฃผ๊ณ ์๋ ๋ฐฉ์์ด์๋ค!!
// footer ์๋ณ์ ์ํ ๋ณ์ ์ ์ธ
private val TYPE_FOOTER = 2
// ์๋ก ๋ค๋ฅธ ๋ทฐ๋ฅผ ๋ฐํํด์ฃผ๊ธฐ ์ํ ํจ์์ด๋ค.
override fun getItemViewType(position: Int): Int {
return when {
isFooter(position) -> TYPE_FOOTER
else -> TYPE_ITEM
}
}
// footer์ item์ ์ํ ๋ทฐํ๋๋ฅผ ์์ฑํด์ค๋ค.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_FOOTER -> FooterViewHolder(footerView!!)
else -> ItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false))
}
// ๋ทฐํ๋์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉํด์ค๋ค.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ItemViewHolder -> {
val itemData = data[getRealPosition(position)] // Adjust position to account for header
holder.bind(itemData)
}
}
}
// footer๋ฅผ ์ํ ๋ทฐํ๋๋ฅผ ๊ตฌํํด์ค๋ค.
class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
span ์กฐ์ ๋ฐฉ์
๋ค์์ span์ ์กฐ์ ํด์ฃผ๋ ๋ฐฉ์์ด๋ค.
์ด ๋ฐฉ์์ ์ฒ์ ์๊ฒ๋์ด์ ์ ๊ธฐํ๋คใ ใ
GridLayoutManager.SpanSizeLookup :๊ฐ ์์ดํ ์ด ์ฐจ์งํ ํ์ ์๋ฅผ ์ ๊ณตํด์ฃผ๋ ์ญํ ์ ํตํด ์ฐจ์งํ๊ณ ์ถ์ ์ด ์ ์๋ฅผ ์กฐ์ ํด์ค ์ ์๋ค!
// spanCount = 3์ธ ๊ฒฝ์ฐ 3์ด์ ์ฐจ์งํ๋๋ก ํ ์ ์๋ค.
val spanCount = 3
val layoutManager = GridLayoutManager(requireContext(), spanCount)
layoutManager.spanSizeLookup =
object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (moviePosterAdapter?.getItemViewType(position)) {
// ๋ก๋ฉ์ ๊ฒฝ์ฐ 3์ด์ ์ฐจ์งํ๋๋ก ํ๊ณ ์ผ๋ฐ ์์ดํ
์ 1์ด์ ์ฐจ์งํ๋๋ก ํด์ฃผ์๋ค.
MoviePosterAdapter.VIEW_TYPE_LOADING -> spanCount
else -> 1
}
์ด๋ ์ถํ ์ด์ ๊ฐ์๊ฐ ๋ค๋ฅธ ํญ ๋ ์ด์์์์ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ๋ฅผ ์ฌ์ฉํ ๋๋ ์ ์ฉํ๊ฒ ์ฌ์ฉ๋์๋ค.
์ฌ๊ธฐ๊น์ง์ ๊ฒฐ๊ณผ๋ ์ง์ ๋ก๋ฉ์ด ์ถ๊ฐ๋์๋ค.(๊ทธ๋ฐ๋ฐ ์์ดํ ์ด ํ ํ ํต์ฑ๋ก ์๋ ๊ฒฝ์ฐ๊ฐ ๋๋ฌผ์ด์ ๋ก๋ฉ๋ ์๋ ์นธ์์๋ง ๋ํ๋ฌ๋ค)
๋์ด ๋๊ฑฐ์ผ? ๋ฐ์ดํฐ๊ฐ ์ค๊ณ ์๋๊ฑฐ์ผ?
๊ทธ๋ฐ๋ฐ ํ๊ฐ์ง ๋ฌธ์ ๊ฐ ์๊ฒผ๋ค. ๋ฐ์ดํฐ๊ฐ ์ค๊ณ ์๋ ์ค์ธ์ง, ๋์ด ๋์ ์ค์ง ์๋์ง ํ๋จํ๋ ๋ก์ง์ ์์ฑํด์ฃผ์ง ์์์
๋ฐ์ดํฐ๋ฅผ ๋ค ๊ฐ์ ธ์จ ๊ฒฝ์ฐ, ์ฆ ์ตํ๋จ์ผ๋ก ์คํฌ๋กค ํ ๊ฒฝ์ฐ์๋ ๋ก๋ฉ์ด ๋ํ๋ฌ๋ค.
๋ง์ง๋ง ํ์ด์ง์ธ์ง๋ฅผ ์ด๋ํฐ์๊ฒ ์๋ ค์ฃผ๋ ๋ก์ง์ด ํ์ํ๋ค.
๋ฐ๋ผ์ ์ด๋ํฐ์ ์๋์ ๊ฐ์ ๋ง์ง๋ง ํ์ด์ง๋ฅผ ์๋ ค์ฃผ๋ ๋ฉ์๋๋ฅผ ์ถ๊ฐํด์ฃผ์๋ค.
fun isLastPage() {
isLastPage = true
}
๋ํ getItemViewType ๋ฉ์๋์์ isLastPage๊ฐ ์๋ ๊ฒฝ์ฐ์๋ง ๋ก๋ฉ ํ๋ฉด์ด ๋จ๋๋ก ํด์ฃผ์๋ค.
override fun getItemViewType(position: Int): Int {
return if (position == posterUriList.size - 1 && !isLastPage) VIEW_TYPE_LOADING else VIEW_TYPE_ITEM
}
ํ๋๊ทธ๋จผํธ ์์์๋ ๋ง์ง๋ง ํ์ด์ง์ผ ๊ฒฝ์ฐ ๋ทฐ๋ชจ๋ธ์์ ํ๋๊ทธ๋จผํธ๋ก NotifyLastPage ๋ผ๋ effect๋ฅผ ๋ณด๋ด๋๋ก ํ๊ณ , ์ด๋ํฐ์ ๋ง์ง๋ง ํ์ด์ง์์ ์๋ ค์ฃผ์๋ค.
is MovieSelectEffect.NotifyLastPage -> {
moviePosterAdapter?.setLoading(false)
moviePosterAdapter?.isLastPage()
Toast.makeText(context, "๋ง์ง๋ง ํ์ด์ง ์
๋๋ค!", Toast.LENGTH_SHORT).show()
์์ผ๋ก๋ ์ด๋ ๊ฒ ๊ตฌํํด์ผํ ๊น???
์ด๋ ๊ฒํ์ฌ ๋ฌดํ์คํฌ๋กค๊ณผ ๋ก๋ฉํ๋ฉด ์ถ๊ฐ๊น์ง ์์ฑ์ ํ๋ค.
๊ทธ๋ฐ๋ฐ ๋ฌดํ์คํฌ๋กค์ ๊ตฌํํ ๋๋ง๋ค ์ด ๊ณผ์ ์ ๋ค ๊ฑฐ์ณ์ผํ ๊น??! ๐คฃ
ํ๋ก์ ํธ์์๋ ๊ฐ์๋ฌธ ๋ฐ์ดํฐ, ๋๊ธ, ์ํ ๋ฑ์ ๋ชจ๋ ๋ฌดํ ์คํฌ๋กค๋ก ๊ตฌํํด์ผํ๋ค.
๊ทธ๋ฐ๋ฐ ์๋์ผ๋ก ๋ค ๊ตฌํํ๋ค๋ณด๋ ํ์ด์ง ์ ์๋ฌ๊ฐ ๋ฐ์ํ ์๋ ์๊ณ ์ฝ๋ ๋ฐ๋ณต์ด ๋ง์์ง๋ค๋ ๋ฌธ์ ๊ฐ ์์๋ค.
๋ฐ๋ผ์ ๊ทธ๋์ ๋ง์ด ๋ค์ด๋ณด์๋ Paging3์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ด ๋ฌด์์ผ๊น๋ฅผ ์ดํด๋ณด๋ค๊ฐ ๋ด๊ฐ ์๋์ผ๋ก ๊ตฌํํ ๊ธฐ๋ฅ๋ค์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ ๊ณตํ๋ค๋ ๊ฒ์ ์๊ฒ๋์๋ค....!!!!!!!!
์์ฒญ๋ฌ๋ค. ๋ฐ๋ผ์ Paging3๋ฅผ ์ ์ฉํด์ ์ผ๋ง๋ ํฐ ์ฐจ์ด๊ฐ ์์์ง ์์๋ณด๊ธฐ๋ก ํ๋ค.
๊ทธ ๊ณผ์ ์ ๋ค์ ๊ธ๋ถํฐ ๊ธฐ๋กํด๋ณด๋๋ก ํ๋ค :)
์คํฌ๋กค ์๋ ๊ตฌํ ๋!! ๊ณ ์ํ๋น
'๐ค2024 ์๋๋ก์ด๋ > ๐ฟ ์ํ ํ๋ก์ ํธ ๊ฐ๋ฐ ์ผ์ง' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋ฌดํ์คํฌ๋กค์ Paging3 ์ ์ฉํ๊ธฐ (9) | 2024.10.13 |
---|---|
๐จ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ์คํฌ๋กค ์ ๋ํ๋ ๊ฒฝ๊ณ (3) | 2024.10.13 |
launch๋ ๊ธฐ๋ค๋ ค์ฃผ์ง ์๋๋ค. (async, await , first etc) (8) | 2024.10.11 |
Android : ๊ฐ์๋ฌธ ๋ฑ๋ก ๊ณผ์ ์ ๋ฆฌ (5) | 2024.08.29 |
๐ ๋ฆฌ์คํธ ์ด๊ธฐํ์ ์ค์์ฑ.. (0) | 2024.08.24 |