ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] RecyclerView DiffUtil
    Android 2020. 5. 24. 16:28

    Intro

    안드로이드에서 Recycler View와 Adpater는 거의 뭐 매번 사용됩니다.

     

    ReyclerView가 갖고 있는 item이 변하게 되면 notifyItemChanged로 알려줘야 하는데요

    그런데 위 메소드가 불러지고 나면 adapter는 새로운 item 인스턴스를 만들어주어야 하기 때문에 비용이 꽤나 많이 듭니다.

    그래서 보다 효율적으로 recyclerView에게 item이 변경되었다고 알려주기 위하여 difftuils라는 utill class 가 생겨났습니다.

     

    본 포스팅으로 Utils class를 조금이나마 알아보겠습니다.

    사용되는 예제 코드는 https://deque.tistory.com/139 포스팅을 참고하였습니다.

    보다 자세한 원문을 보고 싶으면 본 블로그와 깃 헙을 참고하시면 될 것 같습니다. 

     

    What is DiffUtil?

    구글 공식 문서에 보면 DiffUtill 은 두 목록의 차이를 계산하고 old item에서 new Item으로

    목록이 변환할 때 업데이트되는 작업 목록을 출력하는 유틸리티 클래스입니다.

     

    DiffUtill은 Eugene W.Myners의 차이 알고리즘을 사용하여 하나의 목록을 다른 목록으로 변환하기 위한

    최소의 업데이트 수를 계산합니다.

     

    How to Use it?

    class TileDiffUtilCallback(
        private val oldTiles: List<Tile>,
        private val newTiles: List<Tile>
    ) : DiffUtil.Callback() {
    
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return oldTiles[oldItemPosition] == newTiles[newItemPosition]
        }
    
        override fun getOldListSize(): Int {
            return oldTiles.size
        }
    
        override fun getNewListSize(): Int {
            return newTiles.size
        }
    
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return oldTiles[oldItemPosition].number == newTiles[newItemPosition].number
        }
    }
    

     

    DiffUtil.Callback은 4가지 추상 메서드와 1가지 비 추상 메서드로 구성되어있습니다.

    본 예제 코드에서는 4가지 추상 메서드를 오버라이드 하였습니다. 

    또한 DiffUtil 클래스를 사용하기 위해서는 바뀌기 전 리스트와 바뀐 후 리스트 2가지 모두를 알고 있어야 합니다. 

     

    4가지 추상 메서드

    • areItemsTheSame( oldPosition:Int , newPosition:Int) : 두 객체가 동일한 항목을 나타내는지 확인합니다.
    • getOldListSize() : 바뀌 기 전 리스트의 크기를 리턴합니다.
    • getNewListSize() : 바뀐 후 리스트의 크기를 리턴합니다.
    • areContentsTheSame( oldPosition:Int, newPosition:Int) : 두 항목의 데이터가 같은지 확인합니다.
      이 메서드는 areItemsTheSame 이 true일 때만 불립니다. 

    1가지 비 추상 메서드

    • getChangePayload(oldPosition:Int , newPosition:Int)  
      areItemsTheSame == true && areContentsTheSame==false일 경우에 호출됩니다.

    코드 원문은 아래와 같습니다.

    class DiffUtilAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    
        private val dataSet = mutableListOf<Tile>()
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            return TileViewHolder(parent)
        }
    
        override fun getItemCount(): Int = dataSet.size
    
        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            (holder as TileViewHolder).bind(dataSet[position])
        }
    
        private fun setNewTiles(newTiles: MutableList<Tile>) {
            dataSet.run {
                clear()
                addAll(newTiles)
            }
        }
    
        private fun calDiff(newTiles: MutableList<Tile>) {
            val tileDiffUtilCallback = TileDiffUtilCallback(dataSet, newTiles)
            val diffResult: DiffUtil.DiffResult = DiffUtil.calculateDiff(tileDiffUtilCallback)
            diffResult.dispatchUpdatesTo(this)
        }
    
        fun setItems(num: Int) {
            dataSet.clear()
            (1..num).forEach {
                dataSet.add(Tile(it))
            }
        }
    
        fun shuffle() {
            val newTiles = mutableListOf<Tile>().apply {
                addAll(dataSet)
                shuffle()
            }
            calDiff(newTiles)
            setNewTiles(newTiles)
        }
    
        fun addOneTile() {
            val newTiles = mutableListOf<Tile>().apply {
                addAll(dataSet)
            }
            val insertRandomIdx = (Random.nextDouble() * newTiles.size).toInt()
            newTiles.add(insertRandomIdx, Tile(dataSet.size + 1))
            calDiff(newTiles)
            setNewTiles(newTiles)
        }
    
        fun eraseOneTile() {
            val newTiles = mutableListOf<Tile>()
            dataSet.isNotEmpty().let {
                val erasedRandomIdx = (Random.nextDouble() * newTiles.size).toInt()//0
                dataSet.forEachIndexed { index, tile ->
                    if (index != erasedRandomIdx) newTiles.add(tile)
                }
            }
            calDiff(newTiles)
            setNewTiles(newTiles)
        }
    
    
        fun eraseThreeTile() {
            val newTiles = mutableListOf<Tile>().apply {
                addAll(dataSet)
            }
            repeat(3) {
                val erasedRandomIdx = (Random.nextDouble() * newTiles.size).toInt()
                newTiles.removeAt(erasedRandomIdx)
            }
            calDiff(newTiles)
            setNewTiles(newTiles)
        }
    
        fun addThreeTile() {
            val newTiles = mutableListOf<Tile>()
            newTiles.addAll(dataSet)
            repeat(3) {
                val insertRandomIdx = (Random.nextDouble() * newTiles.size).toInt()
                newTiles.add(insertRandomIdx, Tile(newTiles.size + 1))
            }
            calDiff(newTiles)
            setNewTiles(newTiles)
        }
    }
    

     

    중요한 부분을 살펴보겠습니다. 

     private fun calDiff(newTiles: MutableList<Tile>) {
            val tileDiffUtilCallback = TileDiffUtilCallback(dataSet, newTiles)
            val diffResult: DiffUtil.DiffResult = DiffUtil.calculateDiff(tileDiffUtilCallback)
            diffResult.dispatchUpdatesTo(this)
        }

    tileDiffUtilCallback(old item, new item)으로 2개의 아이템을 넣어줍니다. 

    diffReuslt로 결과가 나오는데요.

    diffResult 원문을 보면  아래와 같이 나와있습니다. 

    /**
         * Calculates the list of update operations that can covert one list into the other one.
         * <p>
         * If your old and new lists are sorted by the same constraint and items never move (swap
         * positions), you can disable move detection which takes <code>O(N^2)</code> time where
         * N is the number of added, moved, removed items.
         *
         * @param cb The callback that acts as a gateway to the backing list data
         * @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
         *
         * @return A DiffResult that contains the information about the edit sequence to convert the
         * old list into the new list.
         */
          @NonNull
        public static DiffResult calculateDiff(@NonNull Callback cb, boolean detectMoves) {
        ...
        }

     

     

    주석.. 설명이 참 길죠..? 요약하자면 

    old List가 new List로 변환하기 위한 정보를 포함하여 Return 해준다는 것입니다. 

    이제 diffResult로 나온 값을 dispatchUpdatesTo를 통하여 adpater에게 알려줍니다. 

     

    REF) 

    https://deque.tistory.com/139

     

    Android DiffUtil

    리사이클러뷰를 쓰다가 아이템의 구성이 바뀔때, "아이템이 어디가 바뀌었지... 아... 몰라 notifyDataSetChanged!" 라는 생각을 하며 notifyDataSetChanged()를 호출하곤 해버린다. 물론 좋지 않은 일이지만..

    deque.tistory.com

    https://blog.mindorks.com/the-powerful-tool-diff-util-in-recyclerview-android-tutorial

     

    The powerful tool DiffUtil in RecyclerView - Android Tutorial

    In this blog, we will learn about DiffUtil in RecyclerView. How it is good from the traditional way of using RecyclerView. We will discuss in details

    blog.mindorks.com

     

     https://deque.tistory.com/139를 참고한 코드는
    https://github.com/onemask/PlayGround/tree/features/diffutill에서 확인하실 수 있습니다. 

    LIST

    'Android' 카테고리의 다른 글

    [Android] ViewTreeObserver  (0) 2021.01.03
    [Android] Android Context  (0) 2020.10.04
    [Android] Android Stack 관리 / 확인하기  (0) 2020.02.16
    [Android]FragmentLifeCycle  (0) 2019.07.10
    [Android] 키해시 구하기  (0) 2019.07.06

    댓글

Designed by Tistory.