본문 바로가기

Android 공부/Android Library Study

RecyclerView 더 잘쓰기

 

0. 서론

 RecyclerView는 안드로이드 개발자라면, 질리도록 볼것이고, 계속해서 사용할 것이다. 그래서 대부분의 기능에서 RecyclerView를 요구하고 있고, 이를 커스텀해서 사용하는 경우가 많다. 그래서, 나는 RecyclerView만 다양하게 사용할 수 있다면, 안드로이드의 많은 기능을 구현할 수 있을 것이라고 단언한다. 

 

1. 기본적인 구조

가장 기본적인 구조의 리사이클러뷰

  리사이클러뷰는 리스트뷰에서 출발을 한다. 우선, 리스트뷰와 리사이클러뷰를 비교하자면, 리사이클러뷰를 사용한다고 해서 리스트뷰보다 무조건적인 성능성의 이점을 갖는 것이 아니다. 리스트뷰는 동일한 레이아웃을 갖고 있는 뷰를 뿌려주기 위해서 사용하는 위젯이고, 리스트뷰가 많은 아이템을 갖게 되면 별도의 장치가 없기 때문에 메모리를 절약할 방안이 필요하기 때문에 리사이클러뷰를 사용하는 것이다.

 다시 말해서, 리사이클러뷰는 리스트뷰가 많은 아이템을 갖게 될 때, 혹은 커스텀해서 다양한 기능을 사용하고 싶은 니즈 때문에 점차 발전했다고 생각을 한다. 그래서 ViewHolder 패턴을 통해서 어댑터에서 사용을 하고 있고, ViewHolder는 비교적 비싼 비용을 요구하는 findViewById 결과를 캐싱하기 위한 용도로 리스트를 빠르게 보여줄 수 있도록 도와준다.

 

1-1. Adapter - (Example)

class TodoContentAdapter(val currentList: List<TodoItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return TodoContentHolder(ItemTodoContentBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val content = currentList[position]
        when (holder) {
            is TodoContentHolder -> {
                if (position == 0) {
                    holder.binding.ivTodoCheckBackground.isActivated = true
                }
                holder.bind(content)
            }
            else -> {
                throw Exception("You should not attach here.")
            }
        }
    }

    inner class TodoContentHolder(val binding: ItemTodoContentBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(todoItem: TodoItem) {
            binding.testData = todoItem.title
        }
    }

    override fun getItemCount(): Int = currentList.size
}

팁 1. context는 생성자를 통해서 넘기지 않고, parent.context, view.context와 같이 사용할 수 있다.

아마 이와 같은 방법으로 기본적인 Adapter를 만들 것입니다.

 

1-2. item_layout

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="testData"
            type="String" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        style="@style/TwoLineItemTheme"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/iv_todo_check_background"
            android:layout_width="36dp"
            android:layout_height="36dp"
            android:layout_marginLeft="12dp"
            android:src="@drawable/selector_radius_main_blue_normal_transparent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ImageView
            android:layout_width="16dp"
            android:layout_height="16dp"
            android:src="@drawable/ic_todo_icon"
            app:layout_constraintBottom_toBottomOf="@id/iv_todo_check_background"
            app:layout_constraintLeft_toLeftOf="@id/iv_todo_check_background"
            app:layout_constraintRight_toRightOf="@id/iv_todo_check_background"
            app:layout_constraintTop_toTopOf="@id/iv_todo_check_background" />

        <TextView
            android:id="@+id/tv_todo_title"
            style="@style/WrapTextBodyTextView"
            android:layout_margin="12dp"
            android:text="@{testData}"
            android:textColor="@color/main_black"
            app:layout_constraintLeft_toRightOf="@id/iv_todo_check_background"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="집에서 잠자기" />

        <TextView
            android:id="@+id/tv_todo_content"
            style="@style/WrapCaptionTextView"
            android:layout_marginLeft="12dp"
            android:text="@{testData}"
            android:textColor="@color/gray_400"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@id/iv_todo_check_background"
            app:layout_constraintTop_toBottomOf="@id/tv_todo_title"
            tools:text="집에서 잠자기" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

팁2. tools를 활용하면, item을 레이아웃에서 확인할 수 있다.

tools:listitem="@layout/item_layout"

tools:itemCount="2"

tools:text="example"

 

1-3. Activity

val bottomChoiceTimetableAdapter = BottomChoiceTimetableAdapter(arrayListOf<String>("1","2"))
rv_bottom_share_content.adapter = bottomChoiceTimetableAdapter
rv_bottom_share_content.layoutManager = LinearLayoutManager(context)
bottomChoiceTimetableAdapter.notifyItemChanged(0)
bottomChoiceTimetableAdapter.notifyItemRemoved(0)
bottomChoiceTimetableAdapter.notifyItemInserted(0)

bottomChoiceTimetableAdapter.notifyDataSetChanged() //깜빡임
bottomChoiceTimetableAdapter.setHasStableIds(true) 
bottomChoiceTimetableAdapter.notifyDataSetChanged() //안깜빡임

 리사이클러뷰는 change, remove, insert, move 등과 같이 RecyclerView.Adapter가 제공하고 있는 메소드를 통해서, 어댑터의 데이터가 변할 때, 처리를 할 수 있다. 하지만, adapter를 통해서 notify를 하다보면, 아래와 같은 에러를 마주할 수 있고, 많은 부분을 신경써줘야 하는 것이 골칫거리다.

더보기

android recyclerview inconsistency detected

팁3. notifyDataSetChanged()와 뷰홀더의 id를 통해서 업데이트를 쉽게 하자.

 어댑터에 getItemId()를 통해 id를 부여하고, adapter.setHasStableIds(true)를 통해서 Adapter가 ViewHolder의 id를 통해서 트랙킹하도록 하자. notifyDataSetChanged()를 사용하지 않는 이유중, 하나는 모든 아이템을 업데이트하기 때문에 불필요하다고, 여겨지는 ViewHolder에 id를 등록하게 되면, Adapter가 ViewHolder를 id로 구분지어서 변한 아이템에 대해서만 업데이트를 한다.

필연적으로 사용해야하는 상황이 온다면, 사용해보는 것도 좋은 경험일지도...

 

2. DiffUtil

 RecyclerView가 갖고 있는 아이템이 변하게 되면, 일일히 notify를 해주는 것은 상당히 귀찮다. 그리고, 많은 아이템이 있을 때 해당 아이템을 갖고 작업을 할 때, 조금 더 효율적으로 작업을 하고 싶을 것이다. 이러한 점들을 생각하고 있다면, DiffUtil을 사용해보는 것을 권장한다.

 DiffUtil은 Eugene W. Myers의 difference 알고리즘을 사용한다. DiffUtil을 구현시켜 놓게 되면, 리사이클러뷰에서 사용되어지는 리스트를 갖고, 새로운 리스트가 들어오게 되면, 해당 리스트를 탐지하고, 자동으로 변경해주는 역할을 한다.

 공식문서에는  O(N+ D^2) 만큼의 시간이 걸린다고 한다.

 

100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
100 items and 100 modifications: 3.82 ms, median: 3.75 ms
100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms
Due to implementation constraints, the max size of the list can be 2^26.

 

 라는 결과값이 존재한다. 하지만, 특히 DiffUtil을 사용했을 때, 얻는 이점은 위에서 잠깐 언급했지만, 변한 아이템을 탐지하고, 내부적으로 알아서 notifyItem을 사용해주기 때문에 개발을 할 때, 아이템이 변하는 것에 대해서 많은 신경을 쓰지 않아도 되는 장점이 있다고 생각을 한다.

 

팁4. DiffUtil을 활용해서 데이터 갱신을 위임하자.

class TodoContentAdapter(var todoList: MutableList<TodoItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return TodoContentHolder(ItemTodoContentBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val content = todoList[position]
        when (holder) {
            is TodoContentHolder -> {
                if (position == 0) {
                    holder.binding.ivTodoCheckBackground.isActivated = true
                }
                holder.bind(content)
            }
            else -> {
                throw Exception("You should not attach here.")
            }
        }
    }

    inner class TodoContentHolder(val binding: ItemTodoContentBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(todoItem: TodoItem) {
            binding.testData = todoItem.title
        }
    }

    override fun getItemCount(): Int = todoList.size

    fun update(newItemList: List<TodoItem>) {
        val diffResult = DiffUtil.calculateDiff(ContentDiffUtil(todoList, newItemList), false)
        diffResult.dispatchUpdatesTo(this)
        todoList.clear()
        todoList.addAll(newItemList)
    }
}

class ContentDiffUtil(private val oldList: List<TodoItem>, private val currentList: List<TodoItem>) : DiffUtil.Callback() {
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition] == currentList[newItemPosition]
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == currentList[newItemPosition].id
    }

    override fun getOldListSize(): Int = oldList.size

    override fun getNewListSize(): Int = currentList.size
}

 

3. AsyncListDiffer

 AsyncListDiffer는 DiffUtil을 Wrapping하고 있는 유틸리티 클래스처럼 보인다. 실제로, AsyncListDiffer을 통해서 구현해보면, AsyncListDiffer에게 List의 업데이트 로직까지 공급받다보니, Adapter가 많이 깔끔해보인다.

 실제로 DiffUtil과 어떤 점이 다를까, 검색도 해봤지만, 눈에 띄게 큰 차이는 없었고, AsyncListDiffer에게 Executor를 전달시켜 줄 수 있는 점이 큰 차이라고 생각이 든다. 백그라운드로 DiffUtil 작업을 처리시키면서, '성능성 이점을 가져오지 않을까?' 라고 추측을 해본다.

 

팁5. AsyncListDiffer는 DiffUtil에서 list를 update하는 부분까지 깔끔하게 해준다.

+ Diff 작업을 백그라운드로 하는 것은 분명 성능의 이점을 가져올 것 이다.

class TodoContentAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private val asyncListDiffer = AsyncListDiffer(this, TodoDiffCallback)
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return TodoContentHolder(ItemTodoContentBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val content = asyncListDiffer.currentList[position]
        when (holder) {
            is TodoContentHolder -> {
                if (position == 0) {
                    holder.binding.ivTodoCheckBackground.isActivated = true
                }
                holder.bind(content)
            }
            else -> {
                throw Exception("You should not attach here.")
            }
        }
    }

    inner class TodoContentHolder(val binding: ItemTodoContentBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(todoItem: TodoItem) {
            binding.testData = todoItem.title
        }
    }

    override fun getItemCount(): Int = asyncListDiffer.currentList.size

    fun update(newItemList: List<TodoItem>) = asyncListDiffer.submitList(newItemList)

}

object TodoDiffCallback : DiffUtil.ItemCallback<TodoItem>() {
    override fun areContentsTheSame(oldItem: TodoItem, newItem: TodoItem): Boolean {
        return oldItem == newItem
    }

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

 

4. ListAdapter

 ListAdapter는 AsyncListDiffer을 감싸서 간단하게 사용할 수 있도록 한 클래스이다. 그래서, AsyncDiffConfig를 받을 수도 있고, 흔히 사용하는 DiffUtil.ItemCallback을 받아서 사용하고 있다.

 

팁6. ListAdapter는 간단한 AsyncDiffUtil을 더 간단하게 해준다.

+ ListAdapter를 사용하면, Adapter를 만드는 과정이 많이 축소된다.

class TodoContentAdapter : ListAdapter<TodoItem, RecyclerView.ViewHolder>(TodoDiffCallback) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return TodoContentHolder(ItemTodoContentBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val content = currentList[position]
        when (holder) {
            is TodoContentHolder -> {
                if (position == 0) {
                    holder.binding.ivTodoCheckBackground.isActivated = true
                }
                holder.bind(content)
            }
            else -> {
                throw Exception("You should not attach here.")
            }
        }
    }

    inner class TodoContentHolder(val binding: ItemTodoContentBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(todoItem: TodoItem) {
            binding.testData = todoItem.title
        }
    }
}

object TodoDiffCallback : DiffUtil.ItemCallback<TodoItem>() {
    override fun areContentsTheSame(oldItem: TodoItem, newItem: TodoItem): Boolean {
        return oldItem == newItem
    }

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

//in Activity...
adapter.submitList(list)//아이템을 받아서 리스트 갱신

 

5. recyclerview-selection

 selection은 리사이클러뷰의 아이템을 클릭했을 때, 처리되는 방식을 제공하는 라이브러리이다. selection을 사용하지 않아도, 해당 기능을 구현할 수 있지만, selection 라이브러리를 사용하게 되면, 해당 위치를 기억할 수 있고, 라이브러리가 제공하는 형태로 짜임새있게 구조를 작성할 수 있는 장점이 있다.

 

5-1. Gradle

implementation 'androidx.recyclerview:recyclerview-selection:1.1.0-beta01'

5-2. SelectionTracker

SelectionTracker

- ItemKeyProvider

 ItemKeyProvider는 리사이클러뷰의 아이템을 눌렀을 때, key를 가져오기 위해서 사용하는 클래스이다.

class RecyclerViewIdKeyProvider(private val recyclerView: RecyclerView)
    : ItemKeyProvider<Long>(ItemKeyProvider.SCOPE_MAPPED) {

    override fun getKey(position: Int): Long? {
        return recyclerView.adapter?.getItemId(position)
                ?: throw IllegalStateException("RecyclerView adapter is not set!")
    }

    override fun getPosition(key: Long): Int {
        val viewHolder = recyclerView.findViewHolderForItemId(key)
        return viewHolder?.layoutPosition ?: RecyclerView.NO_POSITION
    }
}

- ItemDetailsLookUp

 ItemDetailsLookUp은 리사이클러뷰의 ItemDetails를 구하기 위한 클래스이다. 해당 ItemDetails에는 position, selectionKey등을 제공해준다.

class ChoiceTimetableLookUp(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
    @Nullable
    override fun getItemDetails(@NonNull motionEvent: MotionEvent): ItemDetails<Long>? {
        val view = recyclerView.findChildViewUnder(motionEvent.x, motionEvent.y)
        if (view != null) {
            val viewHolder =
                    recyclerView.getChildViewHolder(view) as BottomChoiceTimetableAdapter.ChoiceTimetableViewHolder
            return viewHolder.getItemDetails(viewHolder)
        }
        return null
    }
}

- StorageStrategy

 ItemKey를 저장시켜놓는 클래스입니다. ItemKeyProvider에 따라, Long, String, Parcelable 형태로 Stroage를 만들어줍니다.

 

- SelectionPredicates

 단일 아이템을 사용할 것인지, 여러 아이템을 사용할 것인지 선택하는 클래스입니다.

SelectionPredicates.createSelectSingleAnything()//단일
SelectionPredicates.createSelectAnything()//멀티

5-3. 최종적인 코드

 

필수적으로 해야하는 코드

1. setHasStableIds(true) , selection 라이브러리를 사용하기 위해서는 해당 코드를 true로 해야한다. 위에서 tip으로 잠깐 나온 내용으로, id를 통해서 리사이클러뷰의 변함을 트랙킹하기 위해서이다.

2. ViewHolder에는 selectionTracker.isSelected(itemId)으로 해서, true/false를 return 받을 수 있다.

3. ViewHolder에서 아이템을 클릭할 때, selectionTracker.select(itemId)를 사용하게 되면 onBindViewHolder가 재호출된다.

class TodoContentAdapter : ListAdapter<TodoItem, RecyclerView.ViewHolder>(todoDiffCallback) {
    init {
        setHasStableIds(true)
    }

    private lateinit var selectionTracker: SelectionTracker<Long>

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return TodoContentViewHolder(ItemTodoContentBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

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

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val content = currentList[position]
        when (holder) {
            is TodoContentViewHolder -> {
                holder.bind(content, position)
            }
            else -> {
                throw Exception("You should not attach here.")
            }
        }
    }

    fun setSelectionTracker(selectionTracker: SelectionTracker<Long>) {
        this.selectionTracker = selectionTracker
    }

    inner class TodoContentViewHolder(val binding: ItemTodoContentBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(todoItem: TodoItem, itemPosition: Int) {
            binding.testData = todoItem.title
            binding.root.setOnClickListener {
                selectionTracker.select(itemPosition.toLong())
            }
            binding.ivTodoCheckBackground.isActivated = selectionTracker.isSelected(itemPosition.toLong())
            binding.ivTodoCheckIcon.isActivated = selectionTracker.isSelected(itemPosition.toLong())
        }

        fun getItemDetails(viewHolder: RecyclerView.ViewHolder?): ItemDetailsLookup.ItemDetails<Long> {
            return object : ItemDetailsLookup.ItemDetails<Long>() {
                override fun getSelectionKey(): Long? {
                    return itemId
                }

                override fun getPosition(): Int {
                    if (viewHolder == null) {
                        return RecyclerView.NO_POSITION
                    }
                    return viewHolder.adapterPosition
                }
            }
        }
    }
}

class TodoContentDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
    @Nullable
    override fun getItemDetails(@NonNull motionEvent: MotionEvent): ItemDetails<Long>? {
        val view = recyclerView.findChildViewUnder(motionEvent.x, motionEvent.y)
        if (view != null) {
            val viewHolder =
                    recyclerView.getChildViewHolder(view) as TodoContentAdapter.TodoContentViewHolder
            return viewHolder.getItemDetails(viewHolder)
        }
        return null
    }
}

val todoDiffCallback = object : DiffUtil.ItemCallback<TodoItem>() {
    override fun areContentsTheSame(oldItem: TodoItem, newItem: TodoItem): Boolean {
        return oldItem == newItem
    }

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

필수적으로 해야하는 코드

1. adapter를 먼저 선언을 해서 아이템을 만들어야 한다.

2. selectionTracker는 adapter가 만들어진 후에, 연결시켜 준다.

3. selectionTracker는 별도로 adapter에 연결시켜줘야 한다.

val todoContentAdapter = TodoContentAdapter()
binding.rvTodoList.adapter = todoContentAdapter
binding.rvTodoList.layoutManager = LinearLayoutManager(context)
todoContentAdapter.submitList(dummyContent())

val todoContentSelectionTracker = SelectionTracker.Builder<Long>(
	"todo-content",
	binding.rvTodoList,
	StableIdKeyProvider(binding.rvTodoList),
	TodoContentDetailsLookup(binding.rvTodoList),
	StorageStrategy.createLongStorage()
).withSelectionPredicate(SelectionPredicates.createSelectAnything()).build()
todoContentAdapter.setSelectionTracker(todoContentSelectionTracker)

팁7. selection 라이브러리를 사용하면, 선택한 아이템을 다양한 환경에서 유지할 수 있다.

 

 

5-4. 결과

6. 마무리

 리사이클러뷰는 보이는 것처럼 다양하게 사용되고 있다. 해당 기능을 라이브러리를 사용하지 않아도 구현할 수 있겠지만, 항상 사용하는 RecyclerView.Adapter 외에도 필요에 따라 PagedList, selection, ListAdapter와 같이 다양한 라이브러리와 리사이클러뷰를 결합해서 사용할 수 있다는 것을 알았으면 좋겠고, 그와 함께 라이브러리의 구조를 파악해서 사용하면, 좋은 경험이 될 것이라고 생각을 한다.

999. 부록

-Create a List with RecyclerView - https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ViewHolder

 

RecyclerView.ViewHolder  |  Android 개발자  |  Android Developers

RecyclerView.ViewHolder This package is part of the Android support library which is no longer maintained. The support library has been superseded by AndroidX which is part of Jetpack. We recommend using the AndroidX libraries in all new projects. You shou

developer.android.com

- RecyclerViewHolder - https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ViewHolder

 

RecyclerView.ViewHolder  |  Android 개발자  |  Android Developers

RecyclerView.ViewHolder This package is part of the Android support library which is no longer maintained. The support library has been superseded by AndroidX which is part of Jetpack. We recommend using the AndroidX libraries in all new projects. You shou

developer.android.com

 

- Inconsistency Error - https://stackoverflow.com/questions/31603826/recyclerview-crashes-when-updating-on-top

 

RecyclerView crashes when updating on top

I have a RecyclerView in my app everything is working fine I am while scrolling when the last item is visible I am adding some more item to the bottom and it is working fine. Now what the problem i...

stackoverflow.com

- DiffUtil - https://developer.android.com/reference/android/support/v7/util/DiffUtil

 

DiffUtil  |  Android 개발자  |  Android Developers

DiffUtil This package is part of the Android support library which is no longer maintained. The support library has been superseded by AndroidX which is part of Jetpack. We recommend using the AndroidX libraries in all new projects. You should also conside

developer.android.com

- AsyncListDiffer - https://developer.android.com/reference/android/support/v7/recyclerview/extensions/AsyncListDiffer

 

AsyncListDiffer  |  Android 개발자  |  Android Developers

AsyncListDiffer This package is part of the Android support library which is no longer maintained. The support library has been superseded by AndroidX which is part of Jetpack. We recommend using the AndroidX libraries in all new projects. You should also

developer.android.com

- ListAdapter - https://developer.android.com/reference/android/support/v7/recyclerview/extensions/ListAdapter

 

ListAdapter  |  Android 개발자  |  Android Developers

ListAdapter This package is part of the Android support library which is no longer maintained. The support library has been superseded by AndroidX which is part of Jetpack. We recommend using the AndroidX libraries in all new projects. You should also cons

developer.android.com

- selection https://developer.android.com/reference/androidx/recyclerview/selection/package-summary

 

androidx.recyclerview.selection  |  Android 개발자  |  Android Developers

androidx.recyclerview.selection A RecyclerView addon library providing support for item selection. The library provides support for both touch and mouse driven selection. Developers retain control over the visual representation, and the policies controllin

developer.android.com