๊พธ์ค€ํ•จ์ด ์ง„๋ฆฌ๋‹ค!!

์–ด์ œ๋ณด๋‹ค ๋ฐœ์ „ํ•œ ์˜ค๋Š˜์ด ๋˜๊ณ ํ”ˆ ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป ์˜ ๋ธ”๋กœ๊ทธ

Android/Kotlin

[Kotlin][Android] Base RecyclerView Adapter ๋งŒ๋“ค๊ธฐ (feat. Android TV List ๊ตฌํ˜„ํ•˜๊ธฐ)

๋ށ์š” 2024. 1. 12. 14:27

์•ˆ๋…•ํ•˜์„ธ์š”

 

๋„ˆ๋ฌด ์˜ค๋žœ๋งŒ์— ํฌ์ŠคํŒ…์„ ์˜ฌ๋ฆฌ๋„ค์š”!! 

ํ•œ๋™์•ˆ ํšŒ์‚ฌ์—์„œ ๋ฆฌํŒฉํ† ๋ง ๋•Œ๋ฌธ์— ๋„ˆ๋ฌด ์ผ์ด ๋ฐ”๋นด๋„ค์š”..

 

์ด์ œ ๋‹ค์‹œ ๊พธ์ค€ํžˆ ํฌ์ŠคํŒ… ์˜ฌ๋ ค๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!

 

์˜ค๋Š˜์€

.

.

 

์ œ๊ฐ€ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ๋Š” ํ”„๋กœ์ ํŠธ ํŠน์„ฑ์ƒ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•  ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์€๋ฐ์š”!

๊ฒฐ๊ตญ, ํ•ด๋‹น ๋ฆฌ์ŠคํŠธ๋“ค์— ๋™์ผํ•œ ๋™์ž‘์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ฒผ๊ณ ,

๋งค๋ฒˆ ๋™์ž‘๋“ค์„ ์ถ”๊ฐ€ํ•˜๋‹ค ๋ณด๋‹ˆ ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์ด ๋ฐœ์ƒํ•˜์—ฌ ๊ฐ Adapter์˜ BaseClass๋ฅผ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ

๊ด€๋ จํ•ด์„œ ๊ณต์œ ํ•ด๋ณด๋Š” ์‹œ๊ฐ„์„ ๊ฐ€์ง€๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค!

 


 

1. BaseViewHolder ๋งŒ๋“ค๊ธฐ

abstract class BaseViewHolder<ITEM : Any, VB : ViewDataBinding>(binding: VB) :
    RecyclerView.ViewHolder(binding.root) {
    abstract val binding: VB
    abstract fun bind(data: ITEM)
}

 

๋จผ์ €, ViewDataBinding๊ณผ Item์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ์ œ๋„ค๋ฆญ์„ ์ •ํ•ด์ค€ ํ›„ ViewHolder๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„ Bind๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์ถ”์ƒ ํ•จ์ˆ˜๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

2. BaseAdapter ๋งŒ๋“ค๊ธฐ

abstract class BaseAdapter<VB : ViewDataBinding, ITEM : Any>(val layoutId: Int) 
    : RecyclerView.Adapter<BaseViewHolder<ITEM, VB>>() {
        private var itemList = emptyList<ITEM>()
        
        abstract fun createViewHolder(parent: ViewGroup): VB
        
        override fun onBindViewHolder(holder: BaseViewHolder<ITEM, VB>, position: Int) {
            holder.bind(itemList[position])
        }
        
        override fun getItemCount() = itemList.size
        
        fun setItemList(list: List<ITEM>) {
            itemList = list
            notifyItemRangeChanged(0 , itemCount -1)
        }
    
}

 

๋”ฑํžˆ ์–ด๋ ค์šด ๋ถ€๋ถ„์€ ์—†๋Š”๋ฐ์š”, ๊ธฐ๋ณธ์ ์ธ ItemList์™€ BindViewHolder๋ฅผ ๋ฏธ๋ฆฌ ์ •์˜ํ•˜์—ฌ ๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๋ฅผ ์ค„์˜€์Šต๋‹ˆ๋‹ค.

createViewHolder() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ œ๋„ค๋ฆญ์œผ๋กœ ์ง€์ •ํ•œ ViewDataBinding์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์—ฌ DataBinding์„ ์ƒ์„ฑํ•  ๋•Œ ํƒ€์ž…์„ ์ง€์ •ํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š” ๋ฒˆ๊ฑฐ๋กœ์›€์„ ์ค„์˜€์Šต๋‹ˆ๋‹ค.

3. Adapter์— ์ ์šฉํ•˜๊ธฐ

class VideoListAdapter : BaseAdapter<ViewHolderVideoBinding, Video>(R.layout.view_holder_video) {
    override fun createViewHolder(parent: ViewGroup): ViewHolderVideoBinding {
        return DataBindingUtil.inflate(LayoutInflater.from(parent.context), layoutId, parent, false)
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int,
    ): BaseViewHolder<Video, ViewHolderVideoBinding> {
        val binding = createViewHolder(parent)
        return VideoViewHolder(binding)
    }
}

class VideoViewHolder(
    override val binding: ViewHolderVideoBinding,
) : BaseViewHolder<Video, ViewHolderVideoBinding>(binding) {
    override fun bind(data: Video) {
        with(binding) {
            // binding ์ฒ˜๋ฆฌ
        }
    }
}

 

BaseAdapter๋Š” ์œ„์—์„œ ๋ณด์ด๋Š” ๊ฒƒ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„ํ•ด์ฃผ๋ฉด ๋˜๋Š”๋ฐ์š” ์ด๋ ‡๊ฒŒ ๋ณด๋ฉด ๊ตณ์ด ์™œ ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด์•ผ ํ•ด?๋ผ๊ณ  ์ƒ๊ฐ์ด ๋“ค ์ˆ˜๋„ ์žˆ๋Š”๋ฐ์š”

์ด๊ฒƒ์€ ๊ฐœ๋ฐœ์ž๋“ค์˜ ์ทจํ–ฅ์ฐจ์ด๊ฒ ์ง€๋งŒ, ์ €๊ฐ™์€ ๊ฒฝ์šฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ์ด์œ ๋กœ BaseAdapter๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

 

  • ์œ„์—์„œ ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด ๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๋ฅผ ์ค„์—ฌ์„œ ๊ฐœ๋ฐœ์ด ํ›จ์”ฌ ํŽธ๋ฆฌํ•ด์ง„๋‹ค.
  • ๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๊ฐ€ ์ค„๋ฉด์„œ BaseAdapter๋ฅผ ๊ตฌํ˜„ํ•œ ๊ตฌํ˜„์ฒด๋“ค์€ ์ฝ”๋“œ๋Ÿ‰์ด ์ค„์–ด ์ฝ”๋“œ๋ฅผ ๋ณด๊ธฐ ํŽธํ•ด์ง€๊ณ  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ํŽธ๋ฆฌํ•ด์ง„๋‹ค.
  • ๋งˆ์ง€๋ง‰์œผ๋กœ, ์ €๊ฐ™์€ ๊ฒฝ์šฐ๋Š” ๋ชจ๋“  Adapter์— ๊ณตํ†ต ๋™์ž‘(Click , Key ๋“ฑ๋“ฑ)์— ๋Œ€ํ•ด์„œ ๋ฏธ๋ฆฌ ์ •์˜ํ•ด๋‘๊ณ  ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด BaseAdapter๋ฅผ ์ •์˜ํ•ด ๋‘์—ˆ์Šต๋‹ˆ๋‹ค. (์•„๋ž˜๋Š” ์ œ๊ฐ€ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.)

 

abstract class BaseAdapter<VB : ViewDataBinding, ITEM : Any>(
    private val tag: String,
    val layoutId: Int,
    private val recyclerView: RecyclerView,
) : RecyclerView.Adapter<BaseViewHolder<ITEM, VB>>() {

    private var itemList = emptyList<ITEM>()
    private var focusedPosition = 0
    private var itemKeyListener: OnItemKeyListener? = null

    abstract fun createViewHolder(parent: ViewGroup): VB

    override fun onBindViewHolder(holder: BaseViewHolder<ITEM, VB>, position: Int) {
        holder.bind(itemList[position])
        if (position == focusedPosition) {
            holder.itemView.requestFocus()
        }
    }

    override fun getItemCount() = itemList.size

    fun setOnItemKeyListener(listener: OnItemKeyListener) {
        itemKeyListener = listener
    }

    fun setItemList(list: List<ITEM>) {
        itemList = list
        notifyItemRangeChanged(0 , itemCount -1)
    }

    fun requestFocusItem() {
        notifyItemChanged(focusedPosition)
        val layoutManager =
            recyclerView.layoutManager as WrapLinearLayoutManager
        notifyItemChanged(focusedPosition)
        layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
    }

    fun setOnKeyListener(binding: VB) {
        binding.root.setOnKeyListener keyListener@{ _, _, keyEvent ->

            itemKeyListener?.onKeyClick(
                keyEvent.keyCode,
                itemList[focusedPosition],
                focusedPosition,
            )

            when (keyEvent.keyCode) {
                KeyEvent.KEYCODE_DPAD_LEFT -> {
                    if (focusedPosition > 0) {
                        focusedPosition--
                        val layoutManager =
                            recyclerView.layoutManager as WrapLinearLayoutManager
                        notifyItemChanged(focusedPosition)
                        layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
                    }
                    Log.d(tag, "onCreateViewHolder: focusedPosition = $focusedPosition")
                    return@keyListener true
                }

                KeyEvent.KEYCODE_DPAD_RIGHT -> {
                    if (focusedPosition < itemCount - 1) {
                        focusedPosition++
                        val layoutManager =
                            recyclerView.layoutManager as WrapLinearLayoutManager
                        notifyItemChanged(focusedPosition)
                        layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
                    }
                    Log.d(tag, "onCreateViewHolder: focusedPosition = $focusedPosition")
                    return@keyListener true
                }

                KeyEvent.KEYCODE_DPAD_UP,
                KeyEvent.KEYCODE_DPAD_DOWN,
                KeyEvent.KEYCODE_DPAD_CENTER,
                -> {
                    return@keyListener true
                }
            }
            false
        }
    }
}

 

์œ„ ์ฝ”๋“œ๋ฅผ ์ฒœ์ฒœํžˆ ๋ถ„์„ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค! 

 

์šฐ์„ , ํ•ด๋‹น ๋™์ž‘์€ ์ผ๋ฐ˜์ ์ธ Android ๋ชจ๋ฐ”์ผ ๊ธฐ๊ธฐ๋ฅผ ๊ณ ๋ คํ•œ ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๊ณ  Android TV ๋™์ž‘์— ๋Œ€ํ•œ ๋ถ€๋ถ„์ด๋ž€๊ฑธ ์ฐธ๊ณ  ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค!


setOnItemKeyListener()

RecyclerView์—์„œ Key๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์„ ๊ฒฝ์šฐ ํ˜ธ์ถœํ•  Listener๋ฅผ ์„ค์ •ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

fun setOnItemKeyListener(listener: OnItemKeyListener) {
        itemKeyListener = listener
}
interface OnItemKeyListener {
    fun onKeyClick(keyCode: Int, data: Any, index: Int)
}

 

โ€ป ์‚ฌ์šฉ์˜ˆ์‹œ

videoAdapter = VideoListAdapter(rvVideo).apply {
                setOnItemKeyListener(object : OnItemKeyListener {
                    override fun onKeyClick(keyCode: Int, data: Any, index: Int) {
                        when (keyCode) {
                            KeyEvent.KEYCODE_DPAD_DOWN -> {
                                chattingAdapter.requestFocusItem() // ์ฑ„ํŒ…์œผ๋กœ ํฌ์ปค์Šค ์ด๋™
                            }

                            KeyEvent.KEYCODE_DPAD_CENTER -> {
                                if (data is Video) {
                                    mainViewModel.startNewVideo(data) // ๋น„๋””์˜ค ์žฌ์ƒ
                                }
                            }

                            KeyEvent.KEYCODE_DPAD_LEFT -> {
                                if (index == 0) {
                                    mainViewModel.showNavigation() // ๋„ค๋น„๊ฒŒ์ด์…˜ View ํ‘œ์‹œ
                                }
                            }
                        }
                    }
                })
            }

 

requestFocusItem()

์ด์ „์— ์ €์žฅ๋œ focusedPosition์œผ๋กœ Focus๋ฅผ ์ด๋™์‹œํ‚ค๋Š” ํ•จ์ˆ˜

fun requestFocusItem() {
        notifyItemChanged(focusedPosition)
        val layoutManager =
            recyclerView.layoutManager as WrapLinearLayoutManager
        notifyItemChanged(focusedPosition)
        layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
    }

 

setOnKeyListener()

๋ชจ๋“  Adapter์— ๊ณตํ†ต์ ์œผ๋กœ ์ •์˜๋  KeyListener๋ฅผ ๊ตฌํ˜„ํ•œ ํ•จ์ˆ˜

(BaseAdapter๋ฅผ ์ƒ์†๋ฐ›์•„ ์‚ฌ์šฉํ• ๋•Œ ํ•ด๋‹น ์ด๋ฒคํŠธ๋ฅผ ๋“ฑ๋กํ•˜๊ธฐ๋ฅผ ์›์น˜ ์•Š๋Š”๋‹ค๋ฉด onCreateViewHolder์—์„œ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์œผ๋ฉด ๋จ)

fun setOnKeyListener(binding: VB) {
        binding.root.setOnKeyListener keyListener@{ _, _, keyEvent ->
            // ๋ชจ๋“  Key๋Š” ์šฐ์„  Listener๋กœ ์ „๋‹ฌ๋จ
            // ๊ฐ parent์—์„œ ์ฒ˜๋ฆฌํ•  ํ‚ค๋งŒ ์ •์˜ํ•˜์—ฌ ์‚ฌ์šฉ
            itemKeyListener?.onKeyClick(
                keyEvent.keyCode,
                itemList[focusedPosition],
                focusedPosition,
            )

            when (keyEvent.keyCode) {
                KeyEvent.KEYCODE_DPAD_LEFT -> { // ๋ฆฌ์ŠคํŠธ ์™ผ์ชฝ์œผ๋กœ ํฌ์ปค์Šค ์ด๋™
                    if (focusedPosition > 0) {
                        focusedPosition--
                        val layoutManager =
                            recyclerView.layoutManager as WrapLinearLayoutManager
                        notifyItemChanged(focusedPosition)
                        layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
                    }
                    Log.d(tag, "onCreateViewHolder: focusedPosition = $focusedPosition")
                    return@keyListener true
                }

                KeyEvent.KEYCODE_DPAD_RIGHT -> { // ๋ฆฌ์ŠคํŠธ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ํฌ์ปค์Šค ์ด๋™
                    if (focusedPosition < itemCount - 1) {
                        focusedPosition++
                        val layoutManager =
                            recyclerView.layoutManager as WrapLinearLayoutManager
                        notifyItemChanged(focusedPosition)
                        layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
                    }
                    Log.d(tag, "onCreateViewHolder: focusedPosition = $focusedPosition")
                    return@keyListener true
                }

                KeyEvent.KEYCODE_DPAD_UP,
                KeyEvent.KEYCODE_DPAD_DOWN,
                KeyEvent.KEYCODE_DPAD_CENTER,
                -> { // UP , DOWN , CENTER๋Š” Parent์—์„œ ์žฌ์ •์˜
                    return@keyListener true
                }
            }
            false
        }
    }

 

 

์ด๋ ‡๊ฒŒ BaseAdapter๋ฅผ ๊ตฌํ˜„ํ•œ ๋‚ด์šฉ์— ๋Œ€ํ•ด์„œ ํฌ์ŠคํŒ…ํ•ด๋ดค๋Š”๋ฐ์š”!

 

๋‹ค์‹œ ๋ง์”€๋“œ๋ฆฌ์ง€๋งŒ ์ด๊ฑด ๊ฐœ๋ฐœ์ž์˜ ์ทจํ–ฅ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ •๋‹ต์€ ์—†๋‹ค๋Š” ์  ์•Œ์•„์ฃผ์„ธ์š” ๐Ÿ™

 

์ด ํฌ์ŠคํŒ…์ด ์—ฌ๋Ÿฌ๋ถ„์—๊ฒŒ ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค!

 

์˜ค๋Š˜๋„ ์ฆ์ฝ”ํ•˜์„ธ์š” :)