본문 바로가기
안드로이드(kotlin)

DiffUtil를 BaseAdapter로 하여 쉽게 RecyclerView만들기

by 기계공학 주인장 2023. 11. 12.
반응형

옛날에는 안드로이드에서 코틀린을 사용하여 RecyclerView를 만들 때 notifyDataSetChanged를 사용했지만

 

현재는 해당 방법이 deprecated 되었기 때문에 권장하지 않는 방법이 되었습니다.

 

그래서 새로운 방법은 무엇이며 왜 기존 방법이 사용하지 않게 되었는지 설명하겠습니다.


notifyDataSetChanged가 Deprecated 된 이유

이를 알기 위해선 먼저 notifyDataSetChanged의 동작 원리에 대해 알아야 합니다.

 

RecyclerView의 adapter에서 notifyDataSetChanged를 실시하면 다음과 같이 동작합니다.

 

모든 Item에 대해 onBindViewHolder를 실시한다

 

 

즉, 간단하게 말하면 notifyDataSetChanged 사용한 리사이클러뷰의 업데이트는

 

RecyclerView의 모든 아이템의 UI를 새롭게 그린다는 뜻입니다.

 

리사이클러뷰의 아이템이 몇 개 없다면 문제 되지 않지만...

 

아무 많은 아이템을 갖고 있다면 업데이트를 한 번 할 때마다 엄청나게 많은 리소스가 필요하게 될 것입니다.


DiffUtil을 사용하면 뭐가 달라지는데?

DiffUtil을 사용하면 다음과 같은 방식으로 동작합니다.

 

  1. 현재 위치(=Position)의 데이터가 동일한 데이터인지 확인한다.
  2. 해당 데이터가 이전 데이터와 동일한지 비교한다.
  3. 위 1, 2번 중 하나라도 false가 나온다면 해당 위치의 데이터를 교체한다.

코드로 보면 다음과 같습니다.

 

val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {

    // 아이템의 고유 식별자를 비교
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return compare(old[oldItemPosition].hashCode(), new[newItemPosition].hashCode())
    }

    // 아이템의 데이터를 비교
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return old[oldItemPosition] == new[newItemPosition]
    }

    override fun getOldListSize() = old.size

    override fun getNewListSize() = new.size
})

 


DiffUtil을 스마트하게 BaseAdapter로 만들어서 활용하기

먼저 다음과 같이 RecyclerView의 확장 함수를 만듭니다.

 

fun <T> RecyclerView.Adapter<*>.autoNotify(
    old: List<T>,
    new: List<T>,
    compare: (Int, Int) -> Boolean
) {
    val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {

        // 아이템의 고유 식별자를 비교
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return compare(old[oldItemPosition].hashCode(), new[newItemPosition].hashCode())
        }

        // 아이템의 데이터를 비교
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return old[oldItemPosition] == new[newItemPosition]
        }

        override fun getOldListSize() = old.size

        override fun getNewListSize() = new.size
    })
    diff.dispatchUpdatesTo(this)
}



그리고 위에서 만든 autoNotify를 활용하기 위해서 다음과 같은 BaseAdapter를 생성합니다.

 

abstract class BaseDiffRecyclerAdapter<T> :
    RecyclerView.Adapter<BaseDiffRecyclerAdapter.BindingHolder>() {
    var items: List<T> by Delegates.observable(emptyList()) { _, old, new ->
        autoNotify(old, new) { o, n -> o == n }
    }

    class BindingHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
        fun <T : ViewDataBinding> easyBind(receiver: T, block: T.() -> Unit) {
            receiver.block()
            receiver.executePendingBindings()
        }
    }

    companion object {
        fun generateBindingHolder(@LayoutRes layoutRes: Int, parent: ViewGroup?): BindingHolder {
            return BindingHolder(
                DataBindingUtil.inflate(
                    LayoutInflater.from(parent?.context),
                    layoutRes,
                    parent,
                    false
                )
            )
        }
    }
}

 


DiffUtil로 만든 BaseAdapter 활용하기

먼저 테스트로 사용할 TestModel을 만듭니다.

 

data class TestDetailModel(
    var text: String,
    var isClicked: Boolean = false
)

 

그리고 다음과 같이 데이터를 획득하는 함수를 생성합니다.

 

data class TestDetailModel(
    var text: String,
    var isClicked: Boolean = false
)

fun getTestModel() = listOf(
    TestDetailModel("1"),
    TestDetailModel("2"),
    TestDetailModel("3"),
    TestDetailModel("4"),
    TestDetailModel("5"),
    TestDetailModel("6"),
    TestDetailModel("7"),
    TestDetailModel("8"),
    TestDetailModel("9")
)

// 마지막 데이터 삭제
fun deleteSingleTestModel() = listOf(
    TestDetailModel("1"),
    TestDetailModel("2"),
    TestDetailModel("3"),
    TestDetailModel("4"),
    TestDetailModel("5"),
    TestDetailModel("6"),
    TestDetailModel("7"),
    TestDetailModel("8"),
)

// 내부 데이터 수정
fun modifySingleTestModel() = listOf(
    TestDetailModel("1", true),
    TestDetailModel("2", true),
    TestDetailModel("3"),
    TestDetailModel("4"),
    TestDetailModel("5"),
    TestDetailModel("6"),
    TestDetailModel("7"),
    TestDetailModel("8"),
    TestDetailModel("9")
)

// 데이터 재배치
fun arrangeTestModel() = listOf(
    TestDetailModel("1", true),
    TestDetailModel("2", true),
    TestDetailModel("6"),
    TestDetailModel("9"),
    TestDetailModel("4"),
    TestDetailModel("5"),
    TestDetailModel("7"),
    TestDetailModel("8"),
    TestDetailModel("3"),
)

 

그리고 다음과 같이 Adapter를 만듭니다.

 

class TestAdapter : BaseDiffRecyclerAdapter<TestDetailModel>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder =
        generateBindingHolder(R.layout.item_test, parent)

    override fun onBindViewHolder(holder: BindingHolder, position: Int) {
        if (holder.binding is ItemTestBinding) {
            holder.easyBind(holder.binding) {
                checkBox.isChecked = items[position].isClicked
                checkBox.text = items[position].text
            }
        }
    }

    override fun getItemCount() = items.size
}

 

위에서 items는 BaseAdapter에서 정의한 items를 의미합니다.

 


그리고 UI 부분에서는 다음과 같이 정의할 수 있습니다

 

class HomeActivity : AppCompatActivity() {
    private lateinit var binding: ActivityHomeBinding
    private var testModelAdapter = TestAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityHomeBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // UI 초기화
        initScreen()
    }

    private fun initScreen() {
        if (::binding.isInitialized) {
            binding.recycler.let {
                it.layoutManager = LinearLayoutManager(this)
                testModelAdapter.items = getTestModel()
                it.adapter = testModelAdapter
            }
            binding.initBtn.setOnClickListener {
                testModelAdapter.items = getTestModel()
            }
            binding.deleteBtn.setOnClickListener {
                testModelAdapter.items = deleteSingleTestModel()
            }
            binding.modifyBtn.setOnClickListener {
                testModelAdapter.items = modifySingleTestModel()
            }
            binding.arrangeBtn.setOnClickListener {
                testModelAdapter.items = arrangeTestModel()
            }
        } else {
            binding = ActivityHomeBinding.inflate(layoutInflater)
            initScreen()
        }
    }
}

 


결과 화면

반응형


"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."


댓글