(안드로이드) 리사이클러뷰 스크롤시 랜덤한 데이터의 변경 문제.
조회수 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)); } }
댓글 입력