마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

ListAdapter, DiffUtil 사용시 데이터를 한번에 변경하는 방법.

0 추천

Imgur: The magic of the Internet

토글버튼을 클릭시 리사이클러뷰 아이템에 표현된 단위를 한번에 모두 바꾸고 싶은데, 실패하고

고민하다 질문드립니다. 현 상황에 대한 이미지는 링크를 봐주세요.

링크의 이미지에서는 토글버튼을 클릭시 모두 변하는게 아니라 다음 추가되는 아이템에만 적용되어 변합니다.

리사이클러뷰를 사용하고 있고 어댑터는 ListAdapter를 사용하였고 DiffUtil을 함께 사용했습니다.

이 문제를 해결하기위해 제가 하려고 했던것

 

1. 단위는 모든 아이템이 공통적으로 가지는 것이고 한번에 바뀌는 것을 원하기에 단위에 해당하는 변수를

static으로 선언하고 바인딩하고 업데이트 하면되겠다. 

-> 그럴듯 했으나 실패. 처음에는 원인을 몰랐는데 알아보니 static이 클래스에 속하는 변수라 static으로 변

하는 값을 DiffUtill에서는 인지하지 못합니다. 왜냐하면 인스턴스 필드가 아니기때문이죠.

링크의 이미지가 static으로 구현했을때인데 값은 변해서 어댑터에서 바인딩하기때문에

하나씩은 바뀌긴했습니다.

 

2. 토글버튼 클릭시 모든 리스트들을 불러와서 하나하나 필드의 값을 변경된 토글값으로 셋후에 submitList()에 업데이트시켜주기. 

사실 이 방법은 하나하나 리스트들을 불러와서 for문으로 일일이 필드값을 새로 셋팅해줘야하기에

썩 마음에 드는 방법은 아니었지만 제 수준에서는 이것밖에 생각안나서 시도했습니다.

될줄알았는데 계속안되길래 원인을 한참찾다가, DiffUtil에서 값을 비교하는 것이 잘못됐나 싶어서

이것저것 바꾸다가 생각난것이..

토글버튼 클릭했을때 리스트들을 하나하나 불러와서 필드값을 변경했다 했는데, 그 필드가 String 타입이었

으므로 값을 리턴할때 주소를 리턴하게 되는것이고 결국 값을 바꾸면 oldItem의 값도 같이 바뀌어서

DiffUtil에서 oldItem과 newItem을 비교했을때 결국 동일한 아이템취급을 받았던것 같습니다.

그래서 업데이트가 일어나지 않았구요..

 

대충이정도인데 문제는 여기서 어떻게 해결하면 좋을지 모르겠습니다.

리스트를 새로 하나만들고 아이템도 하나하나 새로만들어 값을 세팅하고 리스트에 넣어 업데이트하면

완전 다른 아이템이라 업데이트 될것이라고 예상은 되지만 이렇게 새로 하나하나 다시 만드는게

너무 비효율적이라 생각됩니다.. 조언좀 해주시면 감사하겠습니다.

Activity.java

///// 토글버튼 클릭
@Override
public void onUnitBtnClicked(int curRoutinePos, String unit) {
    Object obj = listAdapter.getRoutineItem(curRoutinePos);
    RoutineModel item = (RoutineModel) obj;
    if(obj instanceof RoutineModel) {
    // 리스트 하나하나 unit 변경하기
        for(RoutineDetailModel detailItem : item.getDetailItemList()) {
             detailItem.setUnit(unit);
        }
        listAdapter.submitList(getUpdatedList());
     }
  }

// 리스트 업데이트
private List<Object> getUpdatedList() {
        List<Object> mixedList = new ArrayList<>();
        for(RoutineModel rm: items){
            mixedList.add(rm);
            if(rm.getDetailItemList() != null && rm.getDetailItemSize() > 0){
                for(RoutineDetailModel rmdetilas: rm.getDetailItemList()){
                    mixedList.add(rmdetilas);
                }
            }
        }
        return mixedList;
    }

 

RoutineDetailModel.java

public class RoutineDetailModel {
    public int id;
    private int set = 1;
    private String weight;
    private String reps;
    public String unit = "kg";

    public RoutineDetailModel() {
        Random random = new Random();
        this.id = random.nextInt();
    }

    public RoutineDetailModel(int set) {
        Random random = new Random();
        this.id = random.nextInt();
        this.set = set+1;
    }

    public int getSet() {
        return set;
    }

    public int getId() {
        return id;
    }


    public void setUnit(String unit) {
        this.unit = unit;
    }

    public String getUnit() {
        return unit;
    }

    @Override
    public int hashCode() {
        return Objects.hash(set, weight);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        RoutineDetailModel that = (RoutineDetailModel) obj;
        return Objects.equals(this.set, that.set) && Objects.equals(this.unit, that.unit);
    }
}

 

DiffUtil.java

public class RoutineDiffUtil2 extends DiffUtil.ItemCallback<Object> {
    // 여기서는 아이템을 구분하는 고유한 값으로 비교하기
    @Override
    public boolean areItemsTheSame(@NonNull Object oldItem, @NonNull Object newItem) {
        if (oldItem instanceof RoutineModel && newItem instanceof RoutineModel) {
            return ((RoutineModel) oldItem).id == ((RoutineModel) newItem).id;
        }
        else if (oldItem instanceof RoutineDetailModel && newItem instanceof RoutineDetailModel) {
            return ((RoutineDetailModel) oldItem).id == ((RoutineDetailModel) newItem).id;
        }
        else if(oldItem instanceof RoutineModel && newItem instanceof RoutineDetailModel) {
            // Routine모델에서 Detail을 꺼내와서 newObj랑 비교해야하나?
            return false;
        }
        else {
            return false;
        }
    }

    // 아이템이 가지고 있는 데이터까지 비교하기
    @SuppressLint("DiffUtilEquals")
    @Override
    public boolean areContentsTheSame(@NonNull Object oldItem, @NonNull Object newItem) {
        return oldItem.equals(newItem);
    }
}

 

codeslave (3,940 포인트) 님이 2021년 3월 25일 질문

2개의 답변

0 추천

for문을 이렇게 쓰지 말고 

// 리스트 하나하나 unit 변경하기
for(RoutineDetailModel detailItem : item.getDetailItemList()) {
  detailItem.setUnit(unit);
}

 

이렇게 써보세요. 

for(int i = 0; i < item.getDetailItemList().size; i++) {
  detailItem.get(i).setUnit(unit)
}

 

쎄미 (162,410 포인트) 님이 2021년 3월 25일 답변
0 추천

제 생각에는  DiffUtil.Callback에 메소드 구현부분을 체크해보시는 것이 좋은 것 같습니다.

areContensTheSame 메소드는 두개의 아이템이 같은 데이터를 가졌는지 비교하기 위한 목적이기 때문에, 님이  토글 필드가 변경된 것을 ListAdapter가 인지하게 하려면 화면에 보여지는 필드들을 비교하셔야 합니다.

@SuppressLint("DiffUtilEquals")
    @Override
    public boolean areContentsTheSame(@NonNull Object oldItem, @NonNull Object newItem) {
        RoutineModel oldOne = (RoutineModel) oldItem;
        RoutineModel newOne = (RoutineModel) newItem; 

        if (oldOne == null || newOne == null) {
           return false;
        }

        return oldOne.id != 0 && oldOne.id == newOne.id &&
        oldOne.weight != null && oldOne.weight.equals(newOne.weight) &&
        oldOne.reps != null && oldOne.reps.equals(newOne.reps) &&
        oldOne.unit != null && oldOne.unit.equals(newOne.units)
    }

 

areItemsTheSame 는 아이템이 동일한지 비교하기 때문에 보통은 equals를 통해 비교를 하게 됩니다. 여기서 주의할 것은 RoutineModel의 equals가 님이 원하는 동작을 하도록 override해야하는지 잘 생각하셔야 한다는 겁니다. 예를 들어 님이 RoutineModel의 id가 같으면 동일한 데이터라고 간주하신 다면, RoutineModel의 equals에서 id필드를 비교하셔야 합니다. 그리고 equals를 오버라이드 하셨다면 map종류의 데이터구조 사용시 생길 수 있는 버그를 방지하기 위해 hashCode도 같이 오버라이드 해주셔야 합니다. 그리고 areitemsTheSame도 먼저 oldItem이나 newItem이 Object이기 때문에 데이터 타입이 같은지 비교하시는게 좋습니다. (추후에 멀티 뷰타입 기능이 추가되거나 하면 다른 클래스를 사용할 가능성이 있으므로, 미리 거기에 따른 문제를 막기 위해서)

@Override
    public boolean areItemsTheSame(@NonNull Object oldItem, @NonNull Object newItem) {
        return oldItem.equals(newItem);
    }

 

spark (224,800 포인트) 님이 2021년 3월 25일 답변
spark님이 2021년 3월 25일 수정
1.areItemsTheSame()에 equals를 사용한다고 하셨는데, 단순 id만 비교하는것이라면 꼭 equals에서 오버라이드해서 사용해야하나요?

구글에서 여러 사이트를 둘러보고 작성을 한것인데 고유한 id나 아이템을 구분하는 고유한 어떤 값이 있다면 그것만 비교하고
equals에서는 이제 데이터를 비교하는 부분을 오버라이드 많이하더라구요.

areItemsTheSame()가 비교하는 두 아이템이 동일한지 비교하는 메소드인데 만약 id가 아이템을 나타내는 고유한 값이라면 저처럼 areItemsTheSame() 내에서 id값만 비교해도 되지 않나요?

그리고 이미 보시면 아시겠지만 if문이 여러개있는데.. 멀티타입을 쓰고있어서 비교대상을 미리미리 조건별로 나눠서 해주고 있습니다.
 (사실 id필드의 경우 저도 고유한 값을 나타내기위해 설정했지만 Random()함수상 낮은 확률로 같은 id가 같은 값이 설정될 수있지만 이 경우는 아직 차치해두고 코드 작성을 진행했습니다. 나중에 DB작성할때 PrimaryKey같은것을 설정하는 방법이 있다면 좋겠지만요.)

따라서 제 수준으로는 문제에대한 해결책이 되는지 잘모르겠습니다 ㅠㅠ

2.책에서도 그렇고 equals를 오버라이드할떄 hashCode()를 항상 같이 오버라이드하던데 이유가 궁금합니다.

Map을 언급해주셨는데  hashCode()를 함께오버라이드하는이유가 HashMap<>에서 데이터를 넣고 빼고할때 이걸 구분하기위한 수단으로 사용된다고 알고있는데
저는 아직 해쉬맵을 사용하고 있지않습니다.

디버깅을해도 호출되는것같지도 않구요. 그런데도 여러곳을 둘러보면
항상 함께 hashCode()를 함께 오버라이드하던데 저한테는 사용, 호출되지도 않는것 같은데 함께 오버라이드 하는 이유가 있나요?

아니면 equals나 뭐 areItemsTheSame이나 areContentsTheSame 같은곳에서
비교할때 내부적으로 뭐 호출되는게 있어서 그러는건가요?
좋은 질문들이네요. 제 생각을 말씀드릴게요.
1. 물론 그렇게도 할 수 있죠. 하지만 잘 생각해 보세요. 님이 DiffUtil.Calback에서 동등성을 비교하는데 해당 로직을 사용한다는 말은 다른 곳에서 동등성을 비교하는 경우에도 같은 코드를 사용해야 한다는 말입니다. 따라서 중복코드를 굳이 사용할 필요가 없고 만약의 경우 동등성 비교 로직이 업데이트 되어야 한다면, 해당 로직을 사용하는 모든 곳을 다 빠짐없이 없데이트 해주어야 합니다. 따라서 동등성을 비교한다면 routineModel.equlas로 비교하는 것이 훨씬 간결하고 좋은 접근방법이라고 생각됩니다.

2. 이유는 Map, Set같은 구조를 사용하게 되면, 내부적으로 hashCode를 가져다 씁니다. 물론 현재는 그런 구조를 사용하지 않기 때문에 아무 문제가 없다고 보실 수도 있으나, 한가지 예로, 님이 몇개월 뒤에 요구사항이 생겨서 Map과 같은 클래스를 사용하게 되었다고 치죠. 그럼 그 때에 님이  RoutineModel의 equals는 오버라이드 되었는데, hashCode는 되지 않았다는 걸 까먹지 않고 기억해낼 수 있을지 장담할 수 없을 겁니다. equals와 hashCode를 동시에 오버라이드하라는 권장사항은 Effective Java에서 나오는데, 아주 숙련된 개발자인 책의 저자가 님과 같이 할 수도 있었는데 귀찮게도 그렇게 권장한 이유는 그의 경험상, equals만 오버라이드 하고 hashCode를 오버라이드 하지 않아 생기는 이상한 증상들을 경험했고 봐왔기 때문이라고 생각합니다. 그리고 코드를 대부분은 팀으로 작업하기 때문에, 이런 권장사항은 습관적으로 지켜주는 것이 좋습니다.  인간은 망각의 동물입니다. 그래서 두 메소드의 함께 오버라이드 해야 한다는 것은 자바에서는 거의 불문율이나 다름없습니다. 버그의 가능성이 보이는 코드를 그냥 두어서는 안되겠죠.
참고로 equals와 hashCode 는 안드로이드 스튜디오 메뉴에 기본적으로 오버라이딩을 해주는 메뉴가 있습니다.
이걸로 기본 코드를 자동으로 만드시고, 필요한 부분만 수정하시면 시간을 절약할 수 있습니다.
1. 감사합니다. 그렇겠네요. 저같은 경우는 RoutineModel, RoutineDetailModel 두 경우 모두 id값으로 아이디를 비교하기때문에 아이디를 비교하는 측면에서는 중복코드가 되겠네요. 말씀대로 수정사항이 생기면 모두 변경해야하구요.

Contents(데이터)를 비교하는 경우에는 모델마다 비교하는 데이터가 다르니 어차피 equals를 사용하나 선생님처럼 작성하나.. 수정사항이 생기면 모두 바꿔야할것같구요..

변경해보겠습니다.

+)그런데 방금 시도해보니 처음 댓글에서 oldItem, newItem 둘다 object타입이라 타입을 구분해주셔야한다했는데 제가 멀티타입을 사용하고있는터라 구분을 해줬더니 ((RoutineModel) oldItem).equals(((RoutineModel) newItem)); 이러한 코드에서는 다 redundant가 뜨네요.. 그러니까 타입캐스팅해주는게 쓸모없다는 말인데.. 알아서 타입구분을해서 equals를 실행하나봐요..

2.이부분에 대해서 항상 궁금해왔었고 이유도 모른채 그냥 했었는데 궁금증이 해결되니 편하네요 감사합니다..
Object 클래스에 equals와 hashCode가 있어요. 님이 사용하시는 클래스에 오버라이드가 안되있다면 부모 클래스인 Object  클래스인 기본 코드가 실행되겠죠.
만약 오버라이드가 되어있다면 object가 타입캐스팅이 자동으로 되어서 실행되죠?..

암튼.. 해봤는데 결관느 여전히 똑같네요 제가 추측했던게 맞았던건지 디버깅을해보니 값을 변경하고 업데이트될때 oldItem과 newItem의 토글 버튼의 값(unit 필드)의 값이 같긴하던데 말이죠
...