Lesson 10: Advanced RecyclerView use cases

Hanbinยท2021๋…„ 9์›” 5์ผ
0

Teach Android Development

๋ชฉ๋ก ๋ณด๊ธฐ
10/13
post-thumbnail

๐Ÿ’ก Teach Android Development

๊ตฌ๊ธ€์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ต์œก์ž๋ฃŒ๋ฅผ ์ •๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ํฌ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.

Android Development Resources for Educators

RecyclerView recap

RecyclerView overview

  • ๋ฐ์ดํ„ฐ list๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•œ ์œ„์ ฏ.
  • ์Šคํฌ๋กค ์„ฑ๋Šฅ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด item view๋ฅผ ์žฌ์‚ฌ์šฉ.
  • dataset์˜ ๊ฐ item์— ๋Œ€ํ•œ layout์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • animation, transition ์ง€์›.

View recycling in RecyclerView

  • item์ด ํ™”๋ฉด ๋ฐ–์œผ๋กœ ์Šคํฌ๋กค ๋˜๋ฉด ํŒŒ๊ดด๋˜์ง€ ์•Š๊ณ  ์žฌํ™œ์šฉ์„ ์œ„ํ•ด pool์— ๋„ฃ์Šต๋‹ˆ๋‹ค.
  • onBindViewHolder()๋Š” View์— ์ƒˆ๋กœ์šด ๊ฐ’์œผ๋กœ ๋ฐ”์ธ๋”ฉ ํ•œ ๋‹ค์Œ list์— ์žฌ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.

Adapter for RecyclerViewDemo

class NumberListAdapter(var data: List<Int>):
        RecyclerView.Adapter<NumberListAdapter.IntViewHolder>() {
   class IntViewHolder(val row: View): RecyclerView.ViewHolder(row) {
       val textView = row.findViewById<TextView>(R.id.number)
   }

Functions for RecyclerViewDemo

onCreateViewHolder()๋ฅผ ํ†ตํ•ด ๋ ˆ์ด์•„์›ƒ์„ inflate ํ•ฉ๋‹ˆ๋‹ค.

onBindViewHolder()๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ bind ํ•ฉ๋‹ˆ๋‹ค.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
              IntViewHolder {
   val layout = LayoutInflater.from(parent.context)
       .inflate(R.layout.item_view, parent, false)
   return IntViewHolder(layout)
}

override fun onBindViewHolder(holder: IntViewHolder, position: Int) {
   holder.textView.text = data.get(position).toString()
}

Set the adapter onto the RecyclerView

Activity์—์„œ RecyclerView์— adapter๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

   val rv: RecyclerView = findViewById(R.id.rv)
   rv.layoutManager = LinearLayoutManager(this)

   rv.adapter = NumberListAdapter(IntRange(0,100).toList())
}

Make items in the list clickable

ViewHolder๊ฐ€ ์ƒ์„ฑ๋  ๋•Œ ํด๋ฆญ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์„ค์ •ํ•ด์ค๋‹ˆ๋‹ค.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IntViewHolder{
    val layout = LayoutInflater.from(parent.context).inflate(R.layout.item_view,
         parent, false)
    val holder = IntViewHolder(layout)
    holder.row.setOnClickListener {
        // Do something on click
    }
    return holder
}

ListAdapter

  • RecyclerView.Adapter
    • ์—…๋ฐ์ดํŠธํ•  ๋•Œ๋งˆ๋‹ค UI ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
    • ๋น„์šฉ์ด ๋งŽ์ด ๋“ค๊ณ  ๋‚ญ๋น„๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ListAdapter
    • ํ˜„์žฌ ํ‘œ์‹œ๋œ ๊ฒƒ๊ณผ ํ‘œ์‹œํ•ด์•ผ ํ•  ๊ฒƒ์˜ ์ฐจ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
    • ๋ณ€๊ฒฝ ์‚ฌํ•ญ์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค.

Sort using RecyclerView.Adapter

Sort using ListAdapter

ListAdapter example

RecyclerView.Adapter<ViewHolder>๋Œ€์‹  ListAdapter<Int, ViewHolder>๋ฅผ extend ํ•ฉ๋‹ˆ๋‹ค.

list ๋น„๊ต ๋ฐฉ๋ฒ•์„ ๊ฒฐ์ •ํ•˜๋Š” DiffUtil.ItemCallback์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

 class NumberListAdapter: ListAdapter<Int,  
        NumberListAdapter.IntViewHolder>(RowItemDiffCallback()) {

    class IntViewHolder(val row: View):RecyclerView.ViewHolder(row) {
        val textView = row.findViewById<TextView>(R.id.number)
    }

    ...

DiffUtil.ItemCallback

ํ˜„์žฌ list๋ฅผ ๋‹ค๋ฅธ list๋กœ ๋ณ€ํ™˜ํ•˜๋Š”๋ฐ ํ•„์š”ํ•œ ๋ฐฉ์‹์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

  • areContentsTheSame(oldItem: T, newItem: T): Boolean
    • ๋™์ผํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
  • areItemsTheSame(oldItem: T, newItem: T): Boolean
    • ๊ธฐ์กด item๊ณผ ๋™์ผํ•œ item ์ธ์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
    • ๊ณ ์œ  ID๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๋™์ผํ•œ์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

DiffUtil.ItemCallback example

class RowItemDiffCallback : DiffUtil.ItemCallback<Int>() {

   override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
       return oldItem.id == newItem.id
   }

   override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
       return oldItem == newItem
   }
}

Advanced binding

ViewHolders and data binding

class IntViewHolder private constructor(val binding: ItemViewBinding):
        RecyclerView.ViewHolder(binding.root) {
    companion object {
        fun from(parent: ViewGroup): IntViewHolder {
            val layoutInflater = LayoutInflater.from(parent.context)
            val binding = ItemViewBinding.inflate(layoutInflater,
                parent, false)
            return IntViewHolder(binding)
        }
    }
}

Using the ViewHolder in a ListAdapter

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
        IntViewHolder {
    return IntViewHolder.from(parent)
}

override fun onBindViewHolder(holder: NumberListAdapter.IntViewHolder,
        position: Int) {
    holder.binding.num = getItem(position)
}

Binding adapters

XML ์†์„ฑ์— ํ•จ์ˆ˜๋ฅผ ๋งคํ•‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด ํ”„๋ ˆ์ž„์›Œํฌ ๋™์ž‘.

android:text = "foo" โ†’  TextView.setText("foo") is called

Custom ์†์„ฑ ์ƒ์„ฑ.

app:base2Number = "5" โ†’ TextView.setBase2Number("5") is called

Custom attribute

Custom ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

<TextView
   android:id="@+id/base2_number"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:textSize="24sp"         
   app:base2Number="@{num}"/>

Add a binding adapter

binding adapter๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

@BindingAdapter("base2Number")
fun TextView.setBase2Number(item: Int) {
    text = Integer.toBinaryString(item)
}

BindingAdapter๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€ ๋กœ๋“œ ๋˜๋Š” ์‹œ๊ฐ„์„ ์†Œ๋ชจํ•˜๋Š” ์ฒ˜๋ฆฌ๊ฐ€ ์žˆ์„ ๋•Œ executePendingBindings()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

override fun onBindViewHolder(holder: NumberListAdapter.IntViewHolder,
        position: Int) {
    holder.binding.num = getItem(position)
    holder.binding.executePendingBindings()
}

Multiple item view types

Add a new item view type

  1. ์ƒˆ๋กœ์šด XML์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  2. ์ƒˆ๋กœ์šด ์œ ํ˜•์„ ๋ณด์œ ํ•˜๋„๋ก adapter๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
  3. getItemViewType() ํ•จ์ˆ˜๋ฅผ ์žฌ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  4. ์ƒˆ๋กœ์šด ViewHolder๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  5. onCreateViewHolder(), onBindViewHolder์— ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ƒˆ ์œ ํ˜•์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

Declare new color item layout

<layout ...>
   <data>
       <variable
           name="color"
           type="android.graphics.Color" />
   </data>
   <androidx.constraintlayout.widget.ConstraintLayout ...>
       <TextView
           ...
           android:backgroundColor="@{color.toArgb()}" />
       <TextView
           ...
           android:text="@{color.toString()}" />
   </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

New view type

  • adapter์—์„œ ๋‘ ๊ฐ€์ง€ ์œ ํ˜•์— ๋Œ€ํ•ด ์•Œ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    • ์ˆซ์ž๋ฅผ ํ‘œ์‹œํ•˜๋Š” ํ•ญ๋ชฉ
    • ์ƒ‰์ƒ์„ ํ‘œ์‹œํ•˜๋Š” ํ•ญ๋ชฉ

    enum class ITEM_VIEW_TYPE { NUMBER, COLOR }

  • getItemViewType()์„ ์ˆ˜์ •ํ•˜์—ฌ ์ ์ ˆํ•œ ์œ ํ˜•์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    override fun getItemViewType(position: Int): Int

Override getItemViewType

override fun getItemViewType(position: Int): Int {
    return when(getItem(position)) {
        is Int -> ITEM_VIEW_TYPE.NUMBER.ordinal
        else -> ITEM_VIEW_TYPE.COLOR.ordinal
    }
}

Define new ViewHolder

class ColorViewHolder private constructor(val binding: ColorItemViewBinding): 
      RecyclerView.ViewHolder(binding.root) {

    companion object {
        fun from(parent: ViewGroup): ColorViewHolder {
            val layoutInflater = LayoutInflater.from(parent.context)
            val binding = ColorItemViewBinding.inflate(layoutInflater,
                parent, false)
            return ColorViewHolder(binding)
        }
    }
}

Update onCreateViewHolder()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
         RecyclerView.ViewHolder {

    return when(viewType) {
        ITEM_VIEW_TYPE.NUMBER.ordinal -> IntViewHolder.from(parent)
        else -> ColorViewHolder.from(parent)
    }
}

Update onBindViewHolder()

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    when (holder) {
        is IntViewHolder -> {
            holder.binding.num = getItem(position) as Int
            holder.binding.executePendingBindings()
        }
        is ColorViewHolder -> {
            holder.binding.color = getItem(position) as Color
            holder.binding.executePendingBindings()
        }
    }
}

Grid layout

Specifying a LayoutManager

  • LinearLayoutManager๋กœ ํ‘œ์‹œํ•  ๊ฒฝ์šฐ
    recyclerView.layoutManager = LinearLayoutManager(this)
  • GridLayoutManager๋กœ ํ‘œ์‹œํ•  ๊ฒฝ์šฐ
    recyclerView.layoutManager = GridLayoutManager(this, 2)
  • ๋‹ค๋ฅธ layout manager ์‚ฌ์šฉ(๋˜๋Š” ์ง์ ‘ ์ƒ์„ฑ)

GridLayoutManager

  • ํ–‰๊ณผ ์—ด์˜ ํ…Œ์ด๋ธ”๋กœ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฐฉํ–ฅ์€ ์„ธ๋กœ, ๊ฐ€๋กœ๋กœ ์Šคํฌ๋กค ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ฐ ํ•ญ๋ชฉ์€ 1 span์„ ์ฐจ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  • ํ•ญ๋ชฉ์ด ์ฐจ์ง€ํ•˜๋Š” span ์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Set span size for an item

SpanSizeLookup ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค๊ณ  getSpanSize(position) ์žฌ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

val manager = GridLayoutManager(this, 2)
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
    override fun getSpanSize(position: Int): Int {
        return when (position) {
            0,1,2 -> 2
            else -> 1
        }
    }
}

0๊ฐœ์˜ ๋Œ“๊ธ€