(안드로이드) 리사이클러뷰 스크롤시 랜덤한 데이터의 변경 문제.

조회수 1330회

이미지

사진과 같은 문제를 겪고 있습니다.

운동일지 작성 앱입니다.

동적으로 아이템을 추가합니다.

처음에 루틴아이템을 추가할 수있고 추가한 루틴 아이템에서 또 버튼을 누르면

상세 아이템이 추가됩니다(세트, 무게, 횟수에관한 아이템. 사진 참조)

따라서 멀티타입 리사이클러뷰를 사용중이고 아이템 변동은 DiffUtil을 사용 중입니다.

그런데 처음에 아이템을 추가하고 데이터를 입력후 아이템을 계속해서 추가하다보면..

(스크롤이 가능할때쯤)

아이템의 데이터를 입력하지도 않았는데 이전에 입력했던 데이터의 값이 들어와있는 상태로

추가됩니다.

게다가 스크롤을 하면 또 랜덤하게 데이터위치가 바뀝니다..

에러가아니라서 디버그도 소용이 없습니다.. 어떻게 해결해야하나요?

RoutineAdapter

public class RoutineAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    final static int TYPE_ROUTINE = 1;
    final static int TYPE_ROUTINE_DETAIL = 2;
    final static int TYPE_ROUTINE_FOOTER = 3;

    private Context context;
    private List<Object> mItems = new ArrayList<>();
    OnRoutineItemClickListener routinelistener;
    OnRoutineAddClickListener routineAddListener;

    public void updateRoutineList(List<Object> newRoutineList) {
        final RoutineDiffUtil diffCallback = new RoutineDiffUtil(this.mItems, newRoutineList);
        final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);

        this.mItems.clear();
        this.mItems.addAll(newRoutineList);
        diffResult.dispatchUpdatesTo(this);
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        context = parent.getContext();
        View itemView;
        if(viewType == TYPE_ROUTINE){
            itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.routine_item, parent, false);
            return new RoutineViewHolder(itemView);
        }
        else if(viewType == TYPE_ROUTINE_DETAIL){
            itemView = LayoutInflater.from(context).inflate(R.layout.routine_detail_item, parent, false);
            return new RoutineDetailViewHolder(itemView);
        }
        else {
            itemView = LayoutInflater.from(context).inflate(R.layout.add_routine_item, parent, false);
            return new RoutineAddFooterViewHolder(itemView);
        }
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        Object obj;
//        holder.setIsRecyclable(false); //이게 맞는지 잘모르겠다
        switch (getItemViewType(position)) {
            case TYPE_ROUTINE:
                obj = mItems.get(position);
                setRoutineData((RoutineViewHolder) holder, (RoutineModel) obj);
                break;
            case TYPE_ROUTINE_DETAIL:
                obj = mItems.get(position);
                RoutineDetailModel item = (RoutineDetailModel) obj;
                ((RoutineDetailViewHolder) holder).setDetailItem(item);
 //                ((RoutineDetailViewHolder) holder).weight.addTextChangedListener(new TextWatcher() {
//                    @Override
//                    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
//
//                    }
//
//                    @Override
//                    public void onTextChanged(CharSequence s, int start, int before, int count) {
//
//                    }
//
//                    @Override
//                    public void afterTextChanged(Editable s) {
//                        item.setWeight(((RoutineDetailViewHolder) holder).weight.getText().toString());
//                    }
//                });
                break;
            case TYPE_ROUTINE_FOOTER:
                break;
        }
    }

    private void setRoutineData(RoutineViewHolder holder, RoutineModel routineItem){
        holder.routine.setText(routineItem.getRoutine());
    }

    public Object getRoutineItem(int position) {
        if(mItems == null || position < 0 || position >= mItems.size())
            return null;
        return mItems.get(position);
    }

    @Override
    public int getItemCount() {
        if(mItems == null)
            return -1;
        return mItems.size() + 1; // footer 때문에 +1
    }

    @Override
    public int getItemViewType(int position) {
        if(position == mItems.size()) { // footer를 마지막에 위치시키기 위함
            return TYPE_ROUTINE_FOOTER;
        }
        else {
            Object obj = mItems.get(position); // 커스텀 LinearlayoutManager의 IOOE 에러가나서 안보이던거였음.
            if(obj instanceof RoutineModel) {
                return TYPE_ROUTINE;
            }
            else {
                // obj instanceof RoutineDetailModel
                return TYPE_ROUTINE_DETAIL;
            }
        }
    }

    // 루틴 추가인터페이스
    public interface OnRoutineAddClickListener {
        public void onAddRoutineClick();
    }

    public void setOnAddRoutineClickListener(OnRoutineAddClickListener listener) {
        this.routineAddListener = listener;
    }

    // 상세 추가/삭제 인터페이스
    public interface OnRoutineItemClickListener {
        public void onAddBtnClicked(int curRoutinePos);
        public void onDeleteBtnClicked(int curRoutinePos);
        public void onWritingCommentBtnClicked(int curRoutinePos);
    }

    public void setOnRoutineClickListener(OnRoutineItemClickListener listener) {
        this.routinelistener = listener;
    }

    private class RoutineViewHolder extends RecyclerView.ViewHolder {
        public TextView routine;
        public Button addSet;
        public Button deleteSet;
        public Button comment;

        public RoutineViewHolder(@NonNull View itemView) {
            super(itemView);

            initViews();

            addSet.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(routinelistener != null && getAdapterPosition() != RecyclerView.NO_POSITION)
                        routinelistener.onAddBtnClicked(getAdapterPosition());
                }
            });

            deleteSet.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(routinelistener != null && getAdapterPosition() != RecyclerView.NO_POSITION)
                        routinelistener.onDeleteBtnClicked(getAdapterPosition());
                }
            });

            comment.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(routinelistener != null && getAdapterPosition() != RecyclerView.NO_POSITION)
                        routinelistener.onWritingCommentBtnClicked(getAdapterPosition());
                }
            });
        }

        private void initViews() {
            routine = itemView.findViewById(R.id.routine);
            addSet = itemView.findViewById(R.id.add_set);
            deleteSet = itemView.findViewById(R.id.delete_set);
            comment = itemView.findViewById(R.id.write_comment);
        }
    }

    private class RoutineDetailViewHolder extends RecyclerView.ViewHolder {
        private TextView set;
        private EditText weight;


        public RoutineDetailViewHolder(@NonNull View itemView) {
            super(itemView);
            initViews();
        }

        private void initViews() {
            set = itemView.findViewById(R.id.set);
            weight = itemView.findViewById(R.id.weight);
        }

        private void setDetailItem(RoutineDetailModel item) {
            set.setText(item.getSet().toString() + "세트");
            weight.setText(item.getWeight());
        }
    }

    private class RoutineAddFooterViewHolder extends RecyclerView.ViewHolder {
        TextView textView;

        public RoutineAddFooterViewHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.add_text);
            ConstraintLayout regionForClick = itemView.findViewById(R.id.clickable_layout);
            regionForClick.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (routineAddListener != null) {
                        routineAddListener.onAddRoutineClick();
                    }
                }
            });
        }
    }
}

2 답변

  • DiffUtil 을 사용하시는데 있어 2가지 오버라이드 할 메서드 중에 areItemsTheSame 과 areContentsTheSame 메서드쪽에서 문제가 있지 않나 싶습니다. DiffUtil 에 넘겨주는 리스트에서는 제네릭으로 Object 를 사용하고 있는데요. 추가될 아이템이 같은것인지, 내용이 같은것인지 확인하기 위해서는 hashCode 와 equals 가 중요합니다.

    리스트 데이터가 Object 가 아닌 정확한 모델 클래스를 사용하시고 DiffUtil 을 구현한 클래스에서 오버라이드한 메서드를 살펴보시기 바랍니다.

    • 감사합니다..지금 글을 확인해보니 움짤을 올렸는데 움짤이 안되나봅니다..https://ibb.co/KDddGKm 현상에 대해 링크로 대체드립니다. 그리고 저도 처음에는 Object 말고 정확한 두개 모델클래스를 사용했는데.. 너무 복잡해져서 그냥 Object에다가 저장하고 타입검사를 하는 방식으로 진행햇습니다.. codeslave 2021.3.16 23:53
    • 그리고 DiffUtil 코드도 답변으로 올려드리겠습니다.. 글 수정이 안돼서.. DiffUtil를 사용할때 아이템을 비교하기 위해서 아이템을 구분할 수있는 변수?같은게 필요하다고 했는데..제 아이템들에는 따로 그걸 지정할만한 내용이 없어서 id를 만들고 random.nextInt()를 사용해서 지정했습니다.. codeslave 2021.3.16 23:58
  • RoutineModel.java.

    public class RoutineModel {
        public int id;
        private ArrayList<RoutineDetailModel> routineDetailList;
        private List<Comment> comments;
        private String routine;
    
        public RoutineModel(String routine) {
            Random random = new Random();
            this.id = random.nextInt();
            this.routine = routine;
        }
    
        public void addDetail(RoutineDetailModel item) {
            if(routineDetailList == null) {
                routineDetailList = new ArrayList<>();
            }
            this.routineDetailList.add(item);
        }
    
        public ArrayList<RoutineDetailModel> getDetailItemList() {
            return routineDetailList;
        }
    
        public int getDetailItemSize() {
            return routineDetailList.size();
        }
    
        public String getRoutine() {
            return routine;
        }
    
        public void removeDetails(int index) throws Exception {
            this.routineDetailList.remove(index);
        }
    
        public void setComments(List<Comment> comments) {
            this.comments = comments;
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(routineDetailList, routine);
        }
    
        @Override
        public boolean equals(@Nullable Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null || getClass() != obj.getClass()) {
                return false;
            }
    
            RoutineModel that = (RoutineModel) obj;
            return Objects.equals(routine, that.routine) && Objects.equals(routineDetailList, that.routineDetailList);
        }
    }
    

    RoutineDetailModel.java

    public class RoutineDetailModel {
        public int id;
        private int set = 1;
        private String weight;
    
        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 Integer getSet() {
            return set;
        }
    
        public int getId() {
            return id;
        }
    
        public void setSet(int set) {
            this.set += set;
        }
    
        public String getWeight() {
            return weight;
        }
    
        public void setWeight(String weight) {
            this.weight = weight;
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(set, weight); // getWeight를 호출하면 더 다양하게 되나?
        }
    
        @Override
        public boolean equals(@Nullable Object obj) {
            if(obj != null && obj instanceof RoutineDetailModel) {
                RoutineDetailModel model = (RoutineDetailModel) obj;
                if(this.id == model.getId()) {
                    return true;
                }
            }
            return false;
        }
    }
    

    DiffUtil.java

    public class RoutineDiffUtil extends DiffUtil.Callback {
        //TODO DiffUtil 클래스 메소드에 대한 설명 주석달기
        private List<Object> oldRoutineList;
        private List<Object> newRoutineList;
    
        public RoutineDiffUtil(List<Object> oldRoutineList, List<Object> newRoutineList) {
            this.oldRoutineList = oldRoutineList;
            this.newRoutineList = newRoutineList;
        }
        @Override
        public int getOldListSize() {
            return oldRoutineList == null ? 0 :oldRoutineList.size();
        }
    
        @Override
        public int getNewListSize() {
             return newRoutineList == null ? 0 : newRoutineList.size();
        }
    
        @Override // 루틴 이름으로 동등비교
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            Object oldObj = oldRoutineList.get(oldItemPosition);
            Object newObj = newRoutineList.get(newItemPosition);
            if (oldObj instanceof RoutineModel && newObj instanceof RoutineModel) {
                return ((RoutineModel) oldObj).id == ((RoutineModel) newObj).id;
            }
            else if (oldObj instanceof RoutineDetailModel && newObj instanceof RoutineDetailModel) {
                return ((RoutineDetailModel) oldObj).id == ((RoutineDetailModel) newObj).id;
            }
            else if(oldObj instanceof RoutineModel && newObj instanceof RoutineDetailModel) {
                // Routine모델에서 Detail을 꺼내와서 newObj랑 비교해야하나?
                return false;
            }
            else {
                return false;
            }
        }
    
        @Override // 루틴 이름과 루틴상세로 동등 비교
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            return (oldRoutineList.get(oldItemPosition)).equals(newRoutineList.get(newItemPosition));
        }
    }
    

답변을 하려면 로그인이 필요합니다.

프로그래머스 커뮤니티는 개발자들을 위한 Q&A 서비스입니다. 로그인해야 답변을 작성하실 수 있습니다.

(ಠ_ಠ)
(ಠ‿ಠ)