oldList 와 newList 리스트의 차이를 계산하고 oldList를 newList로 변환하는 업데이트 작업 목록을 출력 할 수있는 유틸리티 클래스이다.
업데이트 작업 목록은 newList 가 Insert , Remove , Update 다 포함 된다.

리스트 아이템이 변경 되는 갯수 마다의 시간 데이터

  • 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

Eugene W. Myers 의 difference algorithm 을 사용하여 하나의 목록을 다른 목록으로 변환하기위한 최소 업데이트 수를 계산한다.

사용법을 알아보자~!

우선 아래와 같이 DiffUtil.Callback 을 상속받는 DiffCallback 클래스를 만들고 비교할 대상을 지정 해준다.

class DiffCallback(
		private val oldData: AdapterData,
		private val newData: AdapterData
	) : DiffUtil.Callback() {

	override fun getOldListSize() = oldData.size

	override fun getNewListSize() = newData.size

	override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int)
     = oldData[oldItemPosition].id == newData[newItemPosition].id

	override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int)
     = oldData[oldItemPosition] == newData[newItemPosition]

}

다음으로는 RecyclerView Adapter 클래스의 setData 를 보자

fun setData(newData: AdapterData) {
	val diffResult = DiffUtil.calculateDiff(DiffCallback(data, newData))
	data = newData
	diffResult.dispatchUpdatesTo(this)
}

DiffUtil.calculateDiff 는 DiffCallback 을 인자로 받고 결과 값으로 DiffUtil.DiffResult 를 반환 한다.
그 다음 RecyclerView Adapter 의 아이템을 대체 시켜 주고 dispatchUpdatesTo 를 호출한다. 여기서 this는 RecyclerView Adapter 이다.
RecyclerView Adapter 를 파라미터로 받는 이유는 내부적으로 dispatchUpdatesTo 를 호출 하는 순간 뷰가 갱신 된다.
그렇기 때문에 다음 4가지 중 한가지를 호출 하기 위해서라고 판단 된다.

  • mAdapter.notifyItemRangeInserted(position, count);
  • mAdapter.notifyItemRangeRemoved(position, count);
  • mAdapter.notifyItemMoved(fromPosition, toPosition);
  • mAdapter.notifyItemRangeChanged(position, count, payload);

기존 방식은 OldList 에서 변경 된 포지션을 직접 위의 4가지중 한가지를 불러서 뷰를 갱신 시켜줬어야 하는데 DiffUtil 을 사용하면 개발자가 신경 쓰지 않아도 편리하게 수정 할수 있어서 좋은 것 같다.

주의 사항

  1. 리스트 사이즈가 큰 경우는 DiffResult를 가져올때 Background Thread 에서 실행 하고 뷰 갱신 할때만 메인스레드에서 실행 시켜야한다.
  2. DiffUtil 사용 할 때 리스트의 Max Size 는 2^26 이다.

두두(dodo)는 정보통신망 이용촉진 및 정보보호 등에 관한 법률, 개인정보보호법,사업법 등 정보통신서비스제공자가 준수하여야 할 관련 법령상의 개인정보보호 규정을 준수하며, 관련 법령에 의거한 개인정보취급방침을 정하여 이용자 권익 보호에 최선을 다하겠습니다.

두두(dodo)는 사용자 개인정보를 수집 하지 않습니다.

앱 관리자
이름: 김영훈(Davis)
직위: 두두(DoDo) Android Developer
연락처: kimyh8496@gmail.com

고지의 의무 두두(DoDo) 법률이나 서비스의 변경사항을 반영하기 위한 목적 등으로 개인정보 취급방침을 수정할 수 있습니다. 개인정보 취급방침이 변경되는 경우 두두(DoDo)는 변경 사항을 게시하며, 변경된 개인정보 취급방침은 게시한 날로부터 7일 후부터 효력이 발생합니다.

공고일자: 2019년 11월 18일
시행일자: 2019년 12월 25일

기기명 : DS6 (Android 7.1 Nougat)

기본적으로 바코드 트레이라는 앱이 설치 되어있습니다.
이 앱 안에서 스캔 설정을 할 수 있습니다.
스캔 타임, 데이터 수신 방식, 인식 성공시 알림 등등 여러가지가 있습니다.
여기서 중요한 데이터 수신 방식(3가지)에 대해 알아 보겠습니다.

  1. INTERNET_EVENT
  2. KEYBOARD_EVENT
  3. CLIPBOARD_EVENT

INTERNET_EVENT 방식

BroadcastReceiver 로 데이터를 받아 올 수 있습니다.

ActionName : app.dsic.barcodetray.BARCODE_BR_DECODING_DATA

BarcodeData : EXTRA_BARCODE_DECODED_DATA

해당 액션이름으로 이벤트를 받을 수 있고 EXTRA_BARCODE_DECODED_DATA 키 값으로 intent 에 담겨 오는 데이터를 가져올 수 있습니다.

이 방식으로 데이터를 받을 경우 EditText 에 포커스가 되어 있어도 브로드캐스트리시버로 데이터가 넘어 왔기 때문에 EditText 에 입력 되지 않습니다.

KEYBOARD_EVENT 방식

바코드 스캔 할 때 EditText 포커스 된 경우만 데이터가 셋팅 됩니다.

주의 사항
이 방식은 실제로 키보드 타이핑하는 방법과 같습니다.
키보드가 한글로 셋팅 되어 있을 경우 스캔한 내용이 영어일지라도 한글 키보드로 입력한것 처럼 됩니다.
rladudgns0837 내용을 스캔한경우 김영훈0837 이런식으로 입력되기 때문에 주의해야합니다.

키보드가 안보일 경우도 똑같이 적용됩니다.

CLIPBOARD_EVENT 방식

바코드 스캔 할 때 클립보드에 복사되고 EditText에 포커스 되어 있을 경우만 데이터 셋팅 됩니다. 키보드 타이핑 방식은 아니기 때문에 키보드가 영어, 한국어 무관합니다.

FragmentStatePagerAdapter 안에서 사용 되는 Fragment 의 경우 현재 보여지는 Fragment 를 인식 하기 위해

기존 방법(Deprecated)

setUserVisibleHint(isVisibleToUser : Boolean) method를 오버라이드 해서 사용하였다.

새로운 방법

FragmentStatePagerAdapter 생성자부분에서 두번째 파라미터가 생겼는데 BEHAVIOR_SET_USER_VISIBLE_HINT,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 두가지 값으로 나뉘어진다. BEHAVIOR_SET_USER_VISIBLE_HINT 는 deprecated 되었고 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 를 권장 사용하게 한다.

https://developer.android.com/reference/androidx/fragment/app/FragmentStatePagerAdapter.html#BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT

아래와 같이 사용하면 된다~!

Adapter 예시

class MainAdapter(fm: FragmentManager) :
    FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    }

Fragment 예시

override fun onPause() {
        super.onPause()
        //Fragment 가려질 때 처리
    }

    override fun onResume() {
        super.onResume()
        //Fragment 보여질 때 처리
    }

그리고 마지막으로 만약 onResume 이나 onPause가 두번 불리게 된다면 다른 Listener 를 통해 해당 프래그먼트가 불리고 있지 않은지 체크 해봐야한다.

문제

notifyDataSetChanged() 호출 시 RecyclerView 리스트가 갱신이 되지 않는 문제

문제 해결

https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#notifyDataSetChanged()

LayoutManagers will be forced to fully rebind and relayout all visible views.

문서에 의하면 뷰가 Gone 상태 일때는 갱신이 일어나지 않는다.

그래서 Invisible 상태로 두고 갱신시켜 줘서 해결 함.

회고

처음에는 data 가 제대로 들어가 있지 않은것 같아서 확인 했는데 정상적으로 들어가 있었고
두번째는 adapter 의 data list 의 참조 문제를 의심해서 한 곳에서 list 를 만들고 해당 리스트를 다른곳에서도 같이 바라보게 했지만 그대로 문제가 발생
세번째는 뷰가 바인딩 되지 않는 것을 보고 이상하게 생각해서 뷰를 가리지 않고 그대로 하니 잘 나옴..
뷰의 visible 상태에 뭔가 문제가 있다고 생각해서 다큐먼트를 찾아 보니.. 위의 글이 있었다.
좀 더 정확히 알고 사용하는 버릇을 들여야 할 것 같다.