DiffUtil And DragDrop , Slide etc..

DiffUtil And DragDrop , Slide etc..

 
  • RecyclerView, as its name suggests, recycles resources.
  • It is an extension of ListView with improved performance and added features. ReCylerView differs in that it requires the mandatory use of ViewHolder instead of ListView's getView function. More detailed concepts can be found by Googling.
  • I'm writing this post because it's annoying to search Google every time I use RecyclerView's features. I hope my future self sees this and makes it easy.
  • First, we will use the features of RecyclerView by creating a simple app. Below is the UI and features I thought of.
  • When adding an item, it is reflected in the RecyclerView UI.
  • Items can be dragged left or right
  • Change location by swiping an item
  • Item deletion function

Errors and how to resolve them in this article

RecyclerView basic usage

Float the list in RecyclerView && Create a data class to put in RecyclerView

Results screen

notion image

How to implement

  1. Add recyclerview dependency to build.gradle(:app)
    1. dependencies { ... implementation("androidx.recyclerview:recyclerview:1.2.1") }
  1. Add RecyclerView to layout
    1. app/res/layout/activity_main.xml
      <?xml version="1.0" encoding="utf-8"?> <layout> <androidx.constraintlayout.widget.ConstraintLayout 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" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerview" android:layout_width="0dp" android:layout_height="0dp" android:layout_margin="10dp" app:layout_constraintBottom_toTopOf="@+id/add_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/add_button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="10dp" android:text="@string/add_item" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
  1. Create a Data Class that will become an item in RecyclerView
    1. app/java/package/datas/User.kt
      data class User( val id: Int, val name: String) { }
  1. Add item layout file to be drawn in the list
    1. app/res/layout/user_list_item.xml.xml
      <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_margin="10dp" android:layout_height="wrap_content"><ImageView android:id="@+id/user_image" android:layout_width="50dp" android:src="@mipmap/ic_launcher" android:layout_height="50dp" /><TextView android:id="@+id/user_name_text" android:layout_width="wrap_content" android:textSize="20sp" android:gravity="center" android:layout_marginStart="10dp" android:layout_height="match_parent" /></LinearLayout>
  1. Create Adapter class
    1. app/java/package/adapters/UserAdapter
      class UserAdapter( private val mContext: Context, private val mList: MutableList<User> ) : RecyclerView.Adapter<UserAdapter.ViewHolder>() { inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val userImage: ImageView = itemView.findViewById(R.id.user_image) private val userNameText: TextView = itemView.findViewById(R.id.user_name_text) fun bind(user: User) { userNameText.text = user.name } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(mContext).inflate(R.layout.user_list_item, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val user = mList[position] holder.bind(user) } override fun getItemCount() = mList.size }
  1. Connecting adapter to RecyclerView in Kotlin
    1. app/java/package/MainActivity
      class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var adapter: UserAdapter private var userList = mutableListOf<User>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) // RecyclerView에 사용할 데이터 리스트 초기화 for (i in 1 until 11) { val mUser = User(i, "user$i") userList.add(mUser) } } override fun onResume() { super.onResume() // RecyclerView에 리스트 추가 및 어댑터 연결 adapter = UserAdapter(this, userList) binding.recyclerview.layoutManager = LinearLayoutManager(this) binding.recyclerview.adapter = adapter } }

Reflection of RecyclerView UI after adding item

Reflection of RecyclerView UI using diffUtil
  • DiffUtil efficiently reflects the UI when RecyclerView's data changes.
  • When reflecting existing changed data in the UI, notifyDataSetChanged(), notifyItemChanged(), etc. would have been used.
  • notifyDataSetChanged() changes the entire item, making it less efficient the more items there are in the RecyclerView.
  • In addition, there are single item changes (notifyItemChanged(), notifyItemInserted(), notifyItemRemoved(), notifyItemMoved()) and range changes (notifyItemRangeChanged(), notifyItemRangeInserted(), notifyItemRangeRemoved()) .
 

DiffUtil

  • DiffUtil is really smart as it compares lists one by one and returns a Boolean value.
  • If the return value is true, the same item exists in the same position in the two lists.
  • If the return value is false, it is a different item in the same position in the two lists, or it is the same item, but the contents have changed.
Let's use this class to handle efficient UI reflection.

Results screen

You can see that the added items are reflected well.
notion image

How to implement

  1. Add ItemCallback of DiffUtil to RecyclerViewAdpater and change onBindViewHolder to 
    1. val user = mList[position].val user = differ.currentList[position]
      app/java/package/adapters/UserAdapter.kt
      class UserAdapter( private val mContext: Context ) : RecyclerView.Adapter<UserAdapter.ViewHolder>() { private val differCallback = object : DiffUtil.ItemCallback<User>() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { // User의 id를 비교해서 같으면 areContentsTheSame으로 이동(id 대신 data 클래스에 식별할 수 있는 변수 사용) return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { // User의 내용을 비교해서 같으면 true -> UI 변경 없음 // User의 내용을 비교해서 다르면 false -> UI 변경 return oldItem == newItem } } // 리스트가 많으면 백그라운드에서 실행하는 게 좋은데 AsyncListDiffer은 자동으로 백그라운드에서 실행 val differ = AsyncListDiffer(this, differCallback) inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val userImage: ImageView = itemView.findViewById(R.id.user_image) private val userNameText: TextView = itemView.findViewById(R.id.user_name_text) fun bind(user: User) { userNameText.text = user.name } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(mContext).inflate(R.layout.user_list_item, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { // 수정 전 //val user = mList[position]// 수정 후 val user = differ.currentList[position] holder.bind(user) } // 수정 전 // override fun getItemCount() = mList.size// 수정 후 override fun getItemCount() = differ.currentList.size }
  1. In MainActivity, differUtil.submitList()add data to the adapter using . Additionally, a click event is added to the item add button to reflect the Adapter's UI using differUtil when an item is added.
    1. class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var adapter: UserAdapter private var userList = mutableListOf<User>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) for (i in 1 until 11) { val mUser = User(i, "user$i") userList.add(mUser) } } override fun onResume() { super.onResume() adapter = UserAdapter(this) binding.recyclerview.layoutManager = LinearLayoutManager(this) binding.recyclerview.adapter = adapter// DiffUtil 적용 후 데이터 추가 adapter.differ.submitList(userList)// 아이템 추가 버튼 클릭 시 이벤트(userList에 아이템 추가) binding.addButton.setOnClickListener {// 추가할 데이터 생성 val mUser = User( userList.size + 1, "added user ${userList.size + 1}" )// differ의 현재 리스트를 받아와서 newList에 넣기 val newList = adapter.differ.currentList.toMutableList()// newList에 생성한 유저 추가 newList.add(mUser)// adapter의 differ.submitList()로 newList 제출 // submitList()로 제출하면 기존에 있는 oldList와 새로 들어온 newList를 비교하여 UI 반영 adapter.differ.submitList(newList)// userList에도 추가 (꼭 안해줘도 되지만 userList가 필요할때가 있을 수도 있다.) // userList = adapter.differ.currentList 이렇게 사용하면 안됨 userList.add(mUser)// 추가 메시지 출력 Toast.makeText(this, "${mUser.name}이 추가되었습니다.", Toast.LENGTH_SHORT) .show()// 추가된 포지션으로 스크롤 이동 binding.recyclerview.scrollToPosition(userList.indexOf(mUser)) } } }

Change the order of items by dragging

Reorder items using itemTouchHelper
It may get confusing from here, so I'll cut the necessary parts of the code and show you first, and then show you the entire code. We will add drag and drop functionality using itemTouchHelper.

Results screen

notion image

How to implement

  1. Create a class that inherits ItemTouchHelper.SimpleCallback. (It doesn’t matter where the path is)
    1. app/java/package/ItemTouchSimpleCallback
      class ItemTouchSimpleCallback : ItemTouchHelper.SimpleCallback( ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0 ) { interface OnItemMoveListener { fun onItemMove(from: Int, to: Int) } private var listener: OnItemMoveListener? = null fun setOnItemMoveListener(listener: OnItemMoveListener) { this.listener = listener } override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean {// 어댑터 획득 val adapter = recyclerView.adapter as UserAdapter// 현재 포지션 획득 val fromPosition = viewHolder.absoluteAdapterPosition// 옮길 포지션 획득 val toPosition = target.absoluteAdapterPosition// adapter 리스트를 담기위한 변수 생성 val list = arrayListOf<User>()// adapter가 가지고 있는 현재 리스트 획득 list.addAll(adapter.differ.currentList)// 리스트 순서 바꿈 Collections.swap( list, fromPosition, toPosition )// adapter.notifyItemMoved(fromPosition, toPosition)와 같은 역할 // list를 adapter.differ.submitList()로 데이터 변경 사항 알림 adapter.differ.submitList(list)// 추가적인 조치가 필요할 경우 인터페이스를 통해 해결 listener?.onItemMove(fromPosition, toPosition) return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { }// 드래그 완료 후 UI override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { super.clearView(recyclerView, viewHolder)// 순서 조정 완료 후 투명도 다시 1f로 변경 viewHolder.itemView.alpha = 1.0f }// 드래그 중 UI 변화 override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { // 순서 변경 시 alpha를 0.5f viewHolder?.itemView?.alpha = 0.5f } super.onSelectedChanged(viewHolder, actionState) } }
  1. After writing the ItemTouchHelper callback, connect it to RecycleirView in the Activity.
    1. app/java/MainActivity
      • ItemTouchHelper Callback and ItemTouchHelper variable declaration
      private val itemTouchSimpleCallback = ItemTouchSimpleCallback() private val itemTouchHelper = ItemTouchHelper(itemTouchSimpleCallback)
      • Connect ItemTouchHelper and RecyclerView
      // itemTouchSimpleCallback 인터페이스로 추가 작업 itemTouchSimpleCallback.setOnItemMoveListener(object : ItemTouchSimpleCallback.OnItemMoveListener { override fun onItemMove(from: Int, to: Int) { Log.d( "MainActivity", "from Position : $from, to Position : $to" )// userList에도 값이 변하는 걸 원한다면 Collections.swap으로 변경 //Collections.swap(userList, from, to)// userList != adapter.differ.currentList // adapter.differ.currentList는 계속 값을 변경했지만 userList는 변경 전 값(왜냐면 우리는 변경한적이 없다.) Log.d("MainActivity", "userList: $userList") Log.d("MainActivity", "differ currentList: ${adapter.differ.currentList}") } })// itemTouchHelper와 recyclerview 연결 itemTouchHelper.attachToRecyclerView(binding.recyclerview)
      • MainActivity full code
      class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var adapter: UserAdapter private var userList = mutableListOf<User>() private val itemTouchSimpleCallback = ItemTouchSimpleCallback() private val itemTouchHelper = ItemTouchHelper(itemTouchSimpleCallback) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main)// RecyclerView에 사용할 리스트 제공 for (i in 1 until 11) { val mUser = User(i, "user$i") userList.add(mUser) } } override fun onResume() { super.onResume()// RecyclerView에 리스트 추가 및 어댑터 연결 adapter = UserAdapter(this) binding.recyclerview.layoutManager = LinearLayoutManager(this) binding.recyclerview.adapter = adapter// DiffUtil 적용 후 데이터 추가 adapter.differ.submitList(userList)// itemTouchSimpleCallback 인터페이스로 추가 작업 itemTouchSimpleCallback.setOnItemMoveListener(object : ItemTouchSimpleCallback.OnItemMoveListener { override fun onItemMove(from: Int, to: Int) { Log.d("MainActivity", "from Position : $from, to Position : $to") //Collections.swap(userList, from, to)// userList != adapter.differ.currentList // adapter.differ.currentList는 계속 값을 변경했지만 userList는 변경 전 값(왜냐면 우리는 변경한적이 없다.) Log.d("MainActivity", "userList: $userList") Log.d("MainActivity", "differ currentList: ${adapter.differ.currentList}") } })// itemTouchHelper와 recyclerview 연결 itemTouchHelper.attachToRecyclerView(binding.recyclerview)// 아이템 추가 버튼 클릭 시 이벤트(userList에 아이템 추가) binding.addButton.setOnClickListener {// 추가할 데이터 생성 val mUser = User( userList.size + 1, "added user ${userList.size + 1}" )// differ의 현재 리스트를 받아와서 newList에 넣기 val newList = adapter.differ.currentList.toMutableList()// newList에 생성한 유저 추가 newList.add(mUser)// adapter의 differ.submitList()로 newList 제출 // submitList()로 제출하면 기존에 있는 oldList와 새로 들어온 newList를 비교하여 UI 반영 adapter.differ.submitList(newList)// userList에도 추가 (꼭 안해줘도 되지만 userList가 필요할때가 있을 수도 있다.) // userList = adapter.differ.currentList 이렇게 사용하면 안됨 userList.add(mUser)// 추가 메시지 출력 Toast.makeText(this, "${mUser.name}이 추가되었습니다.", Toast.LENGTH_SHORT) .show()// 추가된 포지션으로 스크롤 이동 binding.recyclerview.scrollToPosition(userList.indexOf(mUser)) } } }

Remove an item by clicking the button that appears when fixing the view after swiping

After swiping and fixing the view, remove the item by pressing the delete button hidden behind it.
The task sequence is simple.
  1. Add the view needed to press the delete button after swiping (user_list_item.xml)
  1. Add event to occur when delete button is clicked (UserAdapter.kt)
  1. Add swipe action (ItemTouchSimpleCallback.kt)
  1. Connect additional event listener of ItemTouchSimpleCallback with RecyclerView (MainActivity.kt)
After writing it down, it’s not that simple. Please refer to the comments for explanation! I want to mark the revised part, but I'm not motivated because I've already written it once!

Results screen

notion image

How to implement

  1. Add delete button (I used TextView) and change overall properties (app/res/layout/user_list_item.xml)
    1. app/res/layout/user_list_item.xml
      <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="70dp"> <TextView android:id="@+id/remove_text_view" android:layout_width="100dp" android:layout_height="0dp" android:background="@android:color/holo_red_light" android:gravity="center" android:text="삭제" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <LinearLayout android:id="@+id/swipe_view" android:layout_width="match_parent" android:layout_height="0dp" android:background="@color/white" android:padding="10dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <ImageView android:id="@+id/user_image" android:layout_width="50dp" android:layout_height="50dp" android:src="@mipmap/ic_launcher" /> <TextView android:id="@+id/user_name_text" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_marginStart="10dp" android:gravity="center" android:textSize="20sp" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
  1. Now let's write the event that will occur when the delete button is pressed in the Adapter of RecyclerView (app/java/package/adapters/UserAdapter.kt)
    1. app/java/package/adapters/UserAdapter.kt
      class UserAdapter( private val mContext: Context ) : RecyclerView.Adapter<UserAdapter.ViewHolder>() { inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val swipeView: LinearLayout = itemView.findViewById(R.id.swipe_view) private val userImage: ImageView = itemView.findViewById(R.id.user_image) private val userNameText: TextView = itemView.findViewById(R.id.user_name_text) private val removeTextView: TextView = itemView.findViewById(R.id.remove_text_view) fun bind(user: User) {// 재사용 시 Swipe가 되어있다면 Swipe 원상복구 swipeView.translationX = 0f userNameText.text = user.name removeTextView.setOnClickListener { val list = arrayListOf<User>() list.addAll(differ.currentList) list.remove(user)// 해당 아이템 삭제 adapter에 알리기기 differ.submitList(list) } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(mContext).inflate(R.layout.user_list_item, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { // 수정 전 //val user = mList[position]// 수정 후 val user = differ.currentList[position] holder.bind(user) } override fun getItemCount() = differ.currentList.size// 추가 private val differCallback = object : DiffUtil.ItemCallback<User>() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } } val differ = AsyncListDiffer(this, differCallback) }
  1. Slowly modify ItemTouchSimpleCallback.kt overall. (app/java/package/ItemTouchSimpleCallback.kt)
    1. app/java/package/ItemTouchSimpleCallback.kt
      class ItemTouchSimpleCallback : ItemTouchHelper.SimpleCallback( ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT ) { private var currentPosition: Int? = null private var previousPosition: Int? = null private var currentDx = 0f// 삭제 버튼 width를 넣을 값 private var clamp = 0f interface OnItemMoveListener { fun onItemMove(from: Int, to: Int) } private var listener: OnItemMoveListener? = null fun setOnItemMoveListener(listener: OnItemMoveListener) { this.listener = listener } override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean {// 어댑터 획득 val adapter = recyclerView.adapter as UserAdapter// 현재 포지션 획득 val fromPosition = viewHolder.absoluteAdapterPosition// 옮길 포지션 획득 val toPosition = target.absoluteAdapterPosition// adapter가 가지고 있는 현재 리스트 획득 val list = arrayListOf<User>() list.addAll(adapter.differ.currentList)// 리스트 순서 바꿈 Collections.swap( list, fromPosition, toPosition )// adapter.notifyItemMoved(fromPosition, toPosition) adapter.differ.submitList(list)// 추가적인 조치가 필요할 경우 인터페이스를 통해 해결 listener?.onItemMove(fromPosition, toPosition)return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { } override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { super.clearView(recyclerView, viewHolder)// 순서 조정 완료 후 투명도 다시 1f로 변경 viewHolder.itemView.alpha = 1.0f getDefaultUIUtil().clearView(getView(viewHolder)) previousPosition = viewHolder.absoluteAdapterPosition } override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { // 순서 변경 시 alpha를 0.5f viewHolder?.itemView?.alpha = 0.5f } if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { viewHolder?.let { // 삭제 버튼 width 획득 clamp = getViewWidth(viewHolder) // 현재 뷰홀더 currentPosition = viewHolder.bindingAdapterPosition getDefaultUIUtil().onSelected(getView(it)) } } super.onSelectedChanged(viewHolder, actionState) } override fun onChildDraw( c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { val view = getView(viewHolder) val isClamped = getTag(viewHolder) val x = clampViewPositionHorizontal(view, dX, isClamped, isCurrentlyActive) currentDx = xgetDefaultUIUtil().onDraw( c, recyclerView, view, x, dY, actionState, isCurrentlyActive ) } if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) } }// 삭제버튼 width 구하는 함수 private fun getViewWidth(viewHolder: RecyclerView.ViewHolder): Float { val viewWidth = (viewHolder as UserAdapter.ViewHolder).itemView.findViewById<TextView>(R.id.remove_text_view).width return viewWidth.toFloat() }// swipe될 뷰 (우리가 스와이프할 시 움직일 화면) private fun getView(viewHolder: RecyclerView.ViewHolder): View { return (viewHolder as UserAdapter.ViewHolder).itemView.findViewById(R.id.swipe_view) }// view의 tag로 스와이프 고정됐는지 안됐는지 확인 (고정 == true) private fun getTag(viewHolder: RecyclerView.ViewHolder): Boolean { return viewHolder.itemView.tag as? Boolean ?: false }// view의 tag에 스와이프 고정됐으면 true, 안됐으면 false 값 넣기 private fun setTag(viewHolder: RecyclerView.ViewHolder, isClamped: Boolean) { viewHolder.itemView.tag = isClamped }// 스와이프 될 가로(수평평) 길이 private fun clampViewPositionHorizontal( view: View, dX: Float, // isClamped: Boolean, isCurrentlyActive: Boolean ): Float { val maxSwipe: Float = -clamp * 1.5f val right = 0f val x = if (isClamped) { if (isCurrentlyActive) dX - clamp else -clamp } else dX return min(max(maxSwipe, x), right) }// 사용자가 Swipe 동작으로 간주할 최소 속도 override fun getSwipeEscapeVelocity(defaultValue: Float): Float { return defaultValue * 10 }// 사용자가 스와이프한 것으로 간주할 view 이동 비율 override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { setTag(viewHolder, currentDx <= -clamp) return 2f }// 다른 아이템 클릭 시 기존 swipe되어있던 아이템 원상복구 fun removePreviousClamp(recyclerView: RecyclerView) { if (currentPosition == previousPosition) return previousPosition?.let { val viewHolder = recyclerView.findViewHolderForAdapterPosition(it) ?: return getView(viewHolder).translationX = 0f setTag(viewHolder, false) previousPosition = null } } }
  1. Modify MainActivity.kt to apply removePreviousClamp. (app/java/package/MainActivity.kt)
    1. app/java/package/MainActivity.kt)
      class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var adapter: UserAdapter private var userList = mutableListOf<User>() private val itemTouchSimpleCallback = ItemTouchSimpleCallback() private val itemTouchHelper = ItemTouchHelper(itemTouchSimpleCallback) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main)// RecyclerView에 사용할 리스트 제공 for (i in 1 until 11) { val mUser = User(i, "user$i") userList.add(mUser) } } override fun onResume() { super.onResume()// RecyclerView에 리스트 추가 및 어댑터 연결 adapter = UserAdapter(this) binding.recyclerview.layoutManager = LinearLayoutManager(this) binding.recyclerview.adapter = adapter// DiffUtil 적용 후 데이터 추가 adapter.differ.submitList(userList)// itemTouchSimpleCallback 인터페이스로 추가 작업 itemTouchSimpleCallback.setOnItemMoveListener(object : ItemTouchSimpleCallback.OnItemMoveListener { override fun onItemMove(from: Int, to: Int) { Log.d("MainActivity", "from Position : $from, to Position : $to") //Collections.swap(userList, from, to)// userList != adapter.differ.currentList // adapter.differ.currentList는 계속 값을 변경했지만 userList는 변경 전 값(왜냐면 우리는 변경한적이 없다.) Log.d("MainActivity", "userList: $userList") Log.d("MainActivity", "differ currentList: ${adapter.differ.currentList}") } })// itemTouchHelper와 recyclerview 연결 itemTouchHelper.attachToRecyclerView(binding.recyclerview)// RecyclerView의 다른 곳을 터치하거나 Swipe 시 기존에 Swipe된 것은 제자리로 변경 // 아래 코드가 경고 표시를 주는데 이것은 Annotation @SuppressLint("ClickableViewAccessibility")을 함수에 추가하면 됨 // 또는, performClick 사용 binding.recyclerview.setOnTouchListener { _, _ -> itemTouchSimpleCallback.removePreviousClamp(binding.recyclerview) false }// 아이템 추가 버튼 클릭 시 이벤트(userList에 아이템 추가) binding.addButton.setOnClickListener {// 추가할 데이터 생성 val mUser = User( userList.size + 1, "added user ${userList.size + 1}" )// differ의 현재 리스트를 받아와서 newList에 넣기 val newList = adapter.differ.currentList.toMutableList()// newList에 생성한 유저 추가 newList.add(mUser)// adapter의 differ.submitList()로 newList 제출 // submitList()로 제출하면 기존에 있는 oldList와 새로 들어온 newList를 비교하여 UI 반영 adapter.differ.submitList(newList)// userList에도 추가 (꼭 안해줘도 되지만 userList가 필요할때가 있을 수도 있다.) // userList = adapter.differ.currentList 이렇게 사용하면 안됨 userList.add(mUser)// 추가 메시지 출력 Toast.makeText(this, "${mUser.name}이 추가되었습니다.", Toast.LENGTH_SHORT) .show()// 추가된 포지션으로 스크롤 이동 binding.recyclerview.scrollToPosition(userList.indexOf(mUser)) } } }

MainActivity code cleanup

Let’s clean up the messy source code of MainActivity.
What was in onResume() was divided into functions for each purpose and organized into function calls in onResume().
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var adapter: UserAdapter private var userList = mutableListOf<User>() private val itemTouchSimpleCallback = ItemTouchSimpleCallback() private val itemTouchHelper = ItemTouchHelper(itemTouchSimpleCallback) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView( this, R.layout.activity_main )// RecyclerView에 사용할 리스트 제공 for (i in 1 until 11) { val mUser = User(i, "user$i") userList.add(mUser) } } override fun onResume() { super.onResume() initRecyclerView () setupEvents() } private fun initRecyclerView() { // RecyclerView에 리스트 추가 및 어댑터 연결 adapter = UserAdapter(this) binding.recyclerview.layoutManager = LinearLayoutManager(this) binding.recyclerview.adapter = adapter// DiffUtil 적용 후 데이터 추가 adapter.differ.submitList(userList)// itemTouchSimpleCallback 인터페이스로 추가 작업 itemTouchSimpleCallback.setOnItemMoveListener(object : ItemTouchSimpleCallback.OnItemMoveListener { override fun onItemMove(from: Int, to: Int) { // Collections.swap(userList, from, to) 처럼 from, to가 필요하다면 사용 Log.d("MainActivity", "from Position : $from, to Position : $to") } })// itemTouchHelper와 recyclerview 연결 itemTouchHelper.attachToRecyclerView(binding.recyclerview)// RecyclerView의 다른 곳을 터치하거나 Swipe 시 기존에 Swipe된 것은 제자리로 변경 binding.recyclerview.setOnTouchListener { _, _ -> itemTouchSimpleCallback.removePreviousClamp(binding.recyclerview) false } } private fun setupEvents() { // 아이템 추가 버튼 클릭 시 이벤트(userList에 아이템 추가) binding.addButton.setOnClickListener {// 추가할 데이터 생성 val mUser = User( userList.size + 1, "added user ${userList.size + 1}" )// differ의 현재 리스트를 받아와서 newList에 넣기 val newList = adapter.differ.currentList.toMutableList()// newList에 생성한 유저 추가 newList.add(mUser)// adapter의 differ.submitList()로 newList 제출 // submitList()로 제출하면 기존에 있는 oldList와 새로 들어온 newList를 비교하여 UI 반영 adapter.differ.submitList(newList)// userList에도 추가 (꼭 안해줘도 되지만 userList가 필요할때가 있을 수도 있다.) // userList = adapter.differ.currentList 이렇게 사용하면 안됨 userList.add(mUser)// 추가 메시지 출력 Toast.makeText(this, "${mUser.name}이 추가되었습니다.", Toast.LENGTH_SHORT) .show()// 추가된 포지션으로 스크롤 이동 binding.recyclerview.scrollToPosition(newList.indexOf(mUser)) } } }

When using DiffUtil, the UI is not reflected

If you use DiffUtil.submitList() and the UI is not updated, this article will be useful.
DiffUtil.submitList() refers to the input list and does not have a separate list. What this means is that listA is first inserted into DiffUtil.submitList(). If so, Diffutil is currently looking at listA.
We will now most likely add.(item) to listA to add an Item and put it in DiffUtil.submitList() (I did that! I was stupid). Then, DiffUtil's areItemsTheSame() and areContentsTheSame() return a return value of true. If the return value is true, it means that the items in the same position in the old list and the new list are the same, and the UI is not reflected in the adapter.
Why?
This is because the listA referenced by DifferUtil and the newly introduced listA are the same!
When data comes in through submitList(), DifferUtil compares it with the oldList it is referencing, and this oldList is listA. However, because we have already changed listA to listA.add(item), oldList and newList are in the same situation.
The solution is to add data by putting differ.currentList() in a new variable and put it in submitList().
I will add a link later when I make a post to study this in detail!

reference

In conclusion

The above step-by-step contents were committed to Github. If you need it, go to the address below.