본문 바로가기
Android

RecyclerView + DiffUtil를 이용해 보자! (feat.Kotlin)

by 안솝우화 2022. 4. 13.
반응형

DiffUtil를 알아보기 전에 우선 왜 나오게 되었는지부터 알아보겠습니다

 

 

응애 나 DiffUtil..

기존에 우리는 RecyclerView에서 표시해준 데이터가 변경이 되면 notifyDataSetChanged()를 사용하여 item을 갱신하였습니다.

하지만 notifyDataSetChanged()는 치명적인 단점이 있는데 바로 성능에 악영향을 미치게 된다는 것입니다.

왜냐하면 notifyDataSetChanged()는 기존에 있던 item 리스트를 모두 지우고 다시 새로운 데이터를 모두 하나하나 객체를 생성해 렌더링을 하기 때문입니다. 이렇게 되면 뭐가 문제냐라고 할 수도 있겠지만.. 네, 문제입니다! 비용이 크게 발생하게 되기 때문입니다

따라서 이러한 문제를 해결하기 위해 탄생하게 된 게 바로 오늘 소개할 DiffUtil입니다

 

 

그렇다면 DiffUtil가 뭐야??

DiffUtil는 앞에서 말한 notifyDataSetChanged()와 달리 이전 데이터 상태와 현재 데이터 간의 상태 차이를 계산하고 달라진 부분의 데이터만 갱신하게 됩니다, 때문에 업데이트 횟수가 최소로 줄어들어 딱 필요한 데이터만 갱신이 가능하게 됩니다

 

 

DiffUtil는 어떻게 사용하지??

DiffUtil.Callback이라는 추상 클래스를 사용하여 이용할 수 있습니다, 추상 클래스의 구성은 4개의 추상 메서드, 1개의 비 추상 메서드로 구성되어 있습니다, 하나씩 알아봅시다

  • getOldListSize() : 이전(바뀌지 전) 목록의 개수를 반환합니다
  • getNewListSize() : 새로운(바뀐 후) 목록의 개수를 반환합니다
  • areItemsTheSame(oldItemPosition : Int, newItemPosition : Int) : 두 객체가 같은 항목인지 비교합니다
  • areContentsTheSame(oldItemPosition : Int, newItemPosition : Int) : 두 항목의 데이터가 같은지 비교합니다, 특징은 areContentsTheSame에서 객체를 비교하고 true(같을 때)만 호출됩니다
  • getChangePayload(oldItemPosition : Int, newItemPosition : Int) : 객체를 비교해서 true(같을 때)가 반환되고 데이터를 비교하였는데 false(다를 때)가 반환이 되면 이 메서드가 호출되어 변경 내용을 가져옵니다 (비 추상 메서드)

그럼 코드로 살펴보겠습니다, 모든 설명을 주석으로 대체하겠습니다

아래 예제에서 추가한 종속성입니다

	android {
    	...
        ...
        
    	buildFeatures {
        dataBinding true
    	}
    }
    
    dependencies {
   		...
        ...
        
    	implementation 'androidx.activity:activity-ktx:1.4.0'
    }

종속성을 추가했다면 먼저 User 정보를 담을 Data Class를 만들어줍니다

data class UserInfo(
    val id : String,
    val name : String
)

DiffUtilCallback 클래스를 만들어 줍니다

class UserInfoDiffUtilCallback(
    //oldItem(기존에 있던 데이터), newItem(변경된 데이터)
    private val oldItemList: List<UserInfo>,
    private val newItemList: List<UserInfo>
) : DiffUtil.Callback() {

    override fun getOldListSize() = oldItemList.size

    override fun getNewListSize() = newItemList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        oldItemList[oldItemPosition] == newItemList[newItemPosition]

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        oldItemList[oldItemPosition].id == newItemList[newItemPosition].id
}

데이터를 저장할 ViewModel을 만들어 줍니다

class MainViewModel : ViewModel(){

    var userInfoList = listOf<UserInfo>()
    private var count = 0


    //데이터 가져오기 (계속 1씩 증가)
    fun getList(): MutableList<UserInfo> {
        ++count
        val userInfoList = mutableListOf<UserInfo>()
        for (n in 0 until count) {
            userInfoList.add(UserInfo("userId-$n", "Alice$n"))
        }
        return userInfoList
    }
}

Adapter를 생성해 줍니다

class UserInfoAdapter(
    private val mainViewModel: MainViewModel
) : RecyclerView.Adapter<UserInfoAdapter.UserInfoViewHolder>() {


    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): UserInfoAdapter.UserInfoViewHolder {
        return UserInfoViewHolder(
            RecyclerViewItemBinding.inflate(
                LayoutInflater.from(parent.context),
                parent, false
            )
        )
    }

    override fun onBindViewHolder(holder: UserInfoAdapter.UserInfoViewHolder, position: Int) {
        holder.bind(mainViewModel.userInfoList[position])
    }

    override fun getItemCount() = mainViewModel.userInfoList.size


    fun updateData(newItems: List<UserInfo>) {
        val diffCallback = UserInfoDiffUtilCallback(mainViewModel.userInfoList, newItems)

        //새로운 데이터 호출 결과에 대한 정보를 저장
        val diffResult = DiffUtil.calculateDiff(diffCallback)

        //가져온 새로운 데이터를 다시 userInfoList에 저장
        mainViewModel.userInfoList = newItems

        //업데이트 작업을 전달
        diffResult.dispatchUpdatesTo(this)
    }

    inner class UserInfoViewHolder(
        private val binding: RecyclerViewItemBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(data: UserInfo) {
            binding.data = data
            //즉각적으로 데이터 반영
            binding.executePendingBindings()
        }
    }
}

마지막으로 MainActivity에서 RecyclerView를 호출해 줍니다

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val mainViewModel by viewModels<MainViewModel>()
    private lateinit var userInfoAdapter: UserInfoAdapter


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.activtiy = this

        //초기 데이터 넣기
        mainViewModel.userInfoList = mainViewModel.getList()
        userInfoAdapter = UserInfoAdapter(mainViewModel)

        initRecyclerView()
    }

    //RecyclerView 표시
    private fun initRecyclerView() {
        binding.recyclerView.adapter = userInfoAdapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        binding.recyclerView.setHasFixedSize(true)
    }

    //데이터 변경 버튼 클릭 시
    fun btnClick(view: View) {
        userInfoAdapter.updateData(mainViewModel.getList())
    }
}

 

 

실행된 모습이 궁금하시겠죠?!

 

 

 

DiffUtil, 좋기만 할까?

DiffUtil는 너무 많은 목록을 받아오고 작업을 하게 되면 하나하나 비교 연산을 하기 때문에 작업 시간이 길어질 수가 있습니다, 때문에 공식문서에서는 DiffUtil의 비교 연산을 백그라운드 스레드에서 처리하는 걸 권장합니다
그리고 백그라운드 스레드에서 처리하는 걸 도와주는 AsyncListDiffer가 존재합니다 (글이 길어져 나중에 다루도록 하겠습니다)

 

도움이 되셨다면 공감 버튼 한 번씩 눌러주시면 큰 힘이 됩니다!

반응형