개발일지

Android in A..Z - RecyclerView (payload) 본문

Android (안드로이드)/RecyclerView

Android in A..Z - RecyclerView (payload)

강태종 2021. 1. 10. 19:06

RecyclerView에서 Item변경

RecyclerView에서 Item변경은 Adapter에 notifyItem~ 함수를 호출하면, onBindViewHolder가 호출되어 Item을 변경한다. 하지만 이러한 구조는 특졍 조건을 주어 특정한 부분만 변경하기에는 어렵다

=> 클릭을 했을 때 아이템 상태에 따른 에니메이션주기, View 변화주기 등


Payload

이러한 구조적 문제를 해결하기 위해 notifyItem~ 함수를 호출할 때 payload를 전달하여 특정 조건을 설정해 줄 수 있다.

Adapter에서는 onBindViewHolder(holder, position, payloads)를 구현하여 payload에 따른 특정 부분을 변화시킬 수 있다.

Payload

BaseHolder

abstract class BaseHolder<VB: ViewDataBinding, E: Any>(protected val binding: VB) : RecyclerView.ViewHolder(binding.root) {
    val context: Context
        get() { return itemView.context }

    lateinit var element: E

    open fun bind(element: E) {
        this.element = element
    }

    open fun bind(element: E, payload: MutableList<Any>) {
        this.element = element
    }
}

ToDoHolder

    inner class ToDoHolder(binding: HolderTodoBinding) : BaseHolder<HolderTodoBinding, ToDo>(binding) {
        private var isFinished by Delegates.observable(false) { _, _, newValue ->
            if (newValue) {
                with(binding.isFinished) {
                    animate()
                            .withStartAction {
                                visibility = View.VISIBLE
                            }
                            .alpha(1F)
                            .scaleX(1F)
                            .scaleY(1F)
                            .setDuration(500)
                            .setInterpolator(OvershootInterpolator())
                }
            } else {
                with(binding.isFinished) {
                    animate()
                            .alpha(0F)
                            .scaleX(0F)
                            .scaleY(0F)
                            .setDuration(500)
                            .withEndAction {
                                visibility = View.GONE
                            }
                }
            }
        }

        init {
            itemView.setOnClickListener {
                this@ToDoAdapter.notifyItemChanged(adapterPosition, "onClick")
            }
        }

        private fun onClick() {
            isFinished = !isFinished
        }

        private fun onRefresh() {
            isFinished = false
        }

        override fun bind(element: ToDo) {
            Log.d("PASS", "bind $element")
            super.bind(element)
            binding.todo = element
        }

        override fun bind(element: ToDo, payload: MutableList<Any>) {
            Log.d("PASS", "bind with payload $element, $payload")
            super.bind(element, payload)
            for (any in payload) {
                when(any) {
                    "onClick" -> {
                        onClick()
                    }
                    "onRefresh" -> {
                        onRefresh()
                    }
                }
            }
        }
    }

BaseAdapter

abstract class BaseAdapter<E: Any>(diffCallback: DiffUtil.ItemCallback<E>) : ListAdapter<E, BaseHolder<out ViewDataBinding, E>>(diffCallback) {
    override fun onBindViewHolder(holder: BaseHolder<out ViewDataBinding, E>, position: Int) {
        holder.bind(getItem(position))
    }

    override fun onBindViewHolder(holder: BaseHolder<out ViewDataBinding, E>, position: Int, payloads: MutableList<Any>) {
        super.onBindViewHolder(holder, position, payloads)
        holder.bind(getItem(position), payloads)
    }
}

ToDoAdapter

class ToDoAdapter : BaseAdapter<ToDo>(ToDoItemCallback()) {
    init {
        setHasStableIds(true)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseHolder<out ViewDataBinding, ToDo> {
        return ToDoHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.context), viewType, parent, false))
    }

    override fun getItemId(position: Int): Long {
        return getItem(position).id
    }

    override fun getItemViewType(position: Int): Int {
        return R.layout.holder_todo
    }

    inner class ToDoHolder(binding: HolderTodoBinding) : BaseHolder<HolderTodoBinding, ToDo>(binding) {
        private var isFinished by Delegates.observable(false) { _, _, newValue ->
            if (newValue) {
                with(binding.isFinished) {
                    animate()
                            .withStartAction {
                                visibility = View.VISIBLE
                            }
                            .alpha(1F)
                            .scaleX(1F)
                            .scaleY(1F)
                            .setDuration(500)
                            .setInterpolator(OvershootInterpolator())
                }
            } else {
                with(binding.isFinished) {
                    animate()
                            .alpha(0F)
                            .scaleX(0F)
                            .scaleY(0F)
                            .setDuration(500)
                            .withEndAction {
                                visibility = View.GONE
                            }
                }
            }
        }

        init {
            itemView.setOnClickListener {
                this@ToDoAdapter.notifyItemChanged(adapterPosition, "onClick")
            }
        }

        private fun onClick() {
            isFinished = !isFinished
        }

        private fun onRefresh() {
            isFinished = false
        }

        override fun bind(element: ToDo) {
            Log.d("PASS", "bind $element")
            super.bind(element)
            binding.todo = element
        }

        override fun bind(element: ToDo, payload: MutableList<Any>) {
            Log.d("PASS", "bind with payload $element, $payload")
            super.bind(element, payload)
            for (any in payload) {
                when(any) {
                    "onClick" -> {
                        onClick()
                    }
                    "onRefresh" -> {
                        onRefresh()
                    }
                }
            }
        }
    }

    private class ToDoItemCallback : DiffUtil.ItemCallback<ToDo>() {
        override fun areItemsTheSame(oldItem: ToDo, newItem: ToDo): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: ToDo, newItem: ToDo): Boolean {
            return oldItem.text == newItem.text
        }
    }
}

ToDoFragment

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            R.id.refresh -> {
                adapter.notifyItemRangeChanged(0, adapter.itemCount, "onRefresh")
            }
        }

        return super.onOptionsItemSelected(item)
    }

Payload가 Any(Object)인 이유

Payload를 Any로 받으면 개발자 입장에서 어떠한 자료형이든 매개 변수로 넘길 수 있기 때문에 제약사항이 없이 쉽게 개발할 수 있다.

=> payload를 String, Int, Long등 다양한 자료형을 넘길 수 있으므로 payload에 따른 다양한 조건을 만들 수 있음.

 

Payload를 Adapter에서 MutableList로 받는 이유

onBindViewHolder를 호출하기 전에 여러가지의 payload를 받을 경우 list로 전달하여 한번에 처리하면서 불필요한 리소스를 아낄 수 있다.

 

Payload를 넘길 때 onBindViewHolder 호출 순서

onBindViewHolder(holder, position) -> onBindViewHolder(holder, position, payloads)

=> 기본적인 bind는 onBindViewHolder(holder, position)에 구현하고 특정 부분을 bind하는 부분을 onBindViewHolder(holder, position, payloads)에 구현하면 중복 호출을 막을 수 있다.

 


Git (예제코드)

github.com/KangTaeJong98/Example/tree/main/Android/RecyclerView

 

KangTaeJong98/Example

My Example Code. Contribute to KangTaeJong98/Example development by creating an account on GitHub.

github.com

 

Comments