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

중첩 리사이클러뷰의 동적아이템 추가관련 질문좀 드립니다.

0 추천

현재 제가 만들고 있는 앱의 화면입니다.

루틴(운동) 리사이클러뷰는 각각 동적으로 세트를 추가하는 중첩리사이클러뷰를 가지고 있습니다.

각 운동은 추가하면 기본적으로 바로 1세트가 추가되어있는 상태로 추가됩니다.

그런데 보시다시피 첫운동은 잘 추가가 되는데 이후에 새로운 운동을 추가하면

기존에 추가했던 운동의 세트들이 다 초기화돼고 이후에 추가버튼을 누르면 세트다 

모든 운동에서 같이 증가해버립니다..

저는 각운동에대해 세트를 추가하고싶은데 말이죠.. 단순 리사이클러뷰 안에 리사이클러뷰를

추가하면 되는줄 알았는데 그게 잘 되질않네요..이틀전부터 붙잡고있는데 뭐가 문제인지 잘모르겠습니다.

새 운동을 추가하면 왜 리셋되는지..

RoutineModel.java

public class RoutineModel {
    String routine;
    public ArrayList<RoutineDetailModel> arrayListDetail = new ArrayList<>();

    public RoutineModel(String routine) {
        this.routine = routine;
    }

    public String getRoutine() {
        return routine;
    }
}

RoutineAdapter.java

public class RoutineAdapter extends RecyclerView.Adapter<RoutineAdapter.ViewHolder> {
    ArrayList<RoutineModel> routineItems;
    RoutineDetailAdapter detailAdapter;
    Context context;

    public RoutineAdapter() {
        routineItems = new ArrayList<>();
        detailAdapter = new RoutineDetailAdapter();
    }

    public void addItem(RoutineModel item) {
        routineItems.add(item);
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        context = parent.getContext();
        LayoutInflater inflater = LayoutInflater.from(context);
        View itemView =  inflater.inflate(R.layout.routine_item, parent, false);
        return new ViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        RoutineModel curRoutineItem = routineItems.get(position);
        holder.setItems(curRoutineItem);

        //루틴 상세에 관한 최초 셋팅
        holder.setRoutineDetailRecyClerView();
        curRoutineItem.arrayListDetail.add(new RoutineDetailModel());

        holder.routine_detail.setAdapter(detailAdapter);
        detailAdapter.addItems(curRoutineItem.arrayListDetail);

        holder.addSet.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                detailAdapter.addItems(curRoutineItem.arrayListDetail);
                notifyDataSetChanged();
            }
        });
    }

    @Override
    public int getItemCount() {
        return routineItems.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder  {
        TextView routine;
        Button addSet;
        RecyclerView routine_detail;

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

        private void initViews() {
            routine = itemView.findViewById(R.id.routine);
            routine_detail = itemView.findViewById(R.id.detail_routine);
            addSet = itemView.findViewById(R.id.add_set);

        }
        private void setItems(RoutineModel routineItem) {
            routine.setText(routineItem.getRoutine());
        }

        public void setRoutineDetailRecyClerView() {
            routine_detail.setLayoutManager(new LinearLayoutManager(context, RecyclerView.VERTICAL, false));
            routine_detail.setHasFixedSize(true);
        }
    }
}

RoutineDetailModel.java

public class RoutineDetailModel {
    String set;
    String weight;
    String reps;
}

RoutineDetailAdapter.java

public class RoutineDetailAdapter extends  RecyclerView.Adapter<RoutineDetailAdapter.ViewHolder>{
    ArrayList<RoutineDetailModel> items = new ArrayList<>();

    public void addItems(ArrayList<RoutineDetailModel> items) {
        this.items = items;
        notifyDataSetChanged();
    }

    public ArrayList<RoutineDetailModel> getItem() {
        return this.items;
    }


    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View itemView = inflater.inflate(R.layout.routine_detail_item, parent, false);
        return new ViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        RoutineDetailModel item = items.get(position);
        holder.setItem(item);
    }

    @Override
    public int getItemCount() {
        return items.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView set;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            set = itemView.findViewById(R.id.set);
        }

        private void setItem(RoutineDetailModel item) {
            set.setText("TEST");
        }
    }
}

 

ㄱ그리고 추가적으로 추가버튼 안에 

notifyDatasetChanged()가 있는데.. 이걸 주석처리하면 세트를 추가했을때 반응을안하더라구요..

이유가 궁금합니다..

DetailAdapter 내의 addItem()에서 세트를 추가할때마다 notifyDataSetChanged를 진행해주는데

이거는 반응을안하고 RoutineAdapter내에서 notify를 진행하면 오히려 업데이트되어 화면상에서도 바뀌더라구요

addItems하면서 하는 notifyDataSetChangede가 호출되어야하는게 맞는것 아닌가요?

 

어떻게하면 각 운동에대해서 따로 아이템을 추가할 수잇을까요?

 

 

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

2개의 답변

0 추천

리사이클러뷰를 단순히 2개 사용하시는 거죠. 첫번째 리사이클러뷰에서 루틴을 누르면, 누른 루틴에 해당하는 하위 아이템을 두번째 리사이클러뷰에 보여주는 형태로 이해가 되네요.

가능하면 뷰홀더의 이벤트는 액티비티에서 컨트롤 하셔야 하구요. 아이템의 변경사항은 notify* 메소드를 호출하셔야 화면에 제대로 반영이 됩니다. 그리고 단순히  아이템을 add만 하신거면 notifyItemInserted  같은 별개의 메소드가 있습니다. notifyDataSetChanged는 전체를 다시 그려주는 메소드이므로 굳이 DiffUtil 같은 걸 쓰지 않는다면, 여기에서 쓰실 필요는 없습니다.

public interface ItemClickListener {
     @Override
     public void onItemClicked(RoutineModel routineItem);
}

public class RoutineAdapter extends RecyclerView.Adapter<RoutineAdapter.ViewHolder> 
 
    private ItemClickListener itemClickListener;
    public void setItemClickListener(ItemClickListener itemClickListener) {
           this.itemClickListener = itemClickListener
    }

     @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        context = parent.getContext();
        LayoutInflater inflater = LayoutInflater.from(context);
        View itemView =  inflater.inflate(R.layout.routine_item, parent, false);
        return new ViewHolder(itemView, itemClickListener);
    } 

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        RoutineModel curRoutineItem = routineItems.get(position);
        holder.setItems(curRoutineItem);
    }
 
    public class ViewHolder extends RecyclerView.ViewHolder  {

        private ItemClickListener itemClickListener;
        public ViewHolder(@NonNull View itemView, ItemClickListener itemClickListener) {
            super(itemView);
            this.itemClickListener = itemClickListener;
            initViews();
        }
 
        private void initViews() {
            routine = itemView.findViewById(R.id.routine);
            routine_detail = itemView.findViewById(R.id.detail_routine);
            addSet = itemView.findViewById(R.id.add_set);
        }
        private void setItems(RoutineModel routineItem) {
            routine.setText(routineItem.getRoutine());
            addSet.setOnClickListener(new ItemClickListener() {
                 @Override
                 public void onClick(View view) {
                      itemClickListener.onItemClicked(routineItem);
                 }
            });
        }
    }
}

//RoutineDetailsAdapter

public void setItem(List<RoutineDetailModel items) {
     this.items = items;
}

// Activity
routineAdapter.setItemClickListener(new ItemClickListener() {
     @Override
     public void onItemClicked(RoutineModel routineItem)) {
          routineDetailsAdapter.setitems(routineItem.arrayListDetail);
          routineDetailsAdapter.notifyDataSetChanged();
     }
})


spark (227,510 포인트) 님이 2021년 1월 19일 답변
리사이클러뷰 두개를 사용하실 때 레이아웃을 잘 구성하셔야 할 듯 해요. wrap_content + hasFixedSize를 쓰면 문제가 생길 수 있어요.
감사합니다.. 이상하게 어댑터내에서 notifyItemInserted() 메소드를 호출하니 java.lang.IndexOutOfBoundsException: Inconsistency detected. 어쩌고하는 에러가 나는군요..구글링해보니 저처럼 리사이클러뷰를 사용하다가 나는 분들이 계신데 정확히 어디서 나는지 안나오네요..일단은 인터페이스를 사용해서 클릭이벤트를 액티비티에서 구현해줄생각인데 그래도 난다면 다시 뭔가를 바꾸어 보아야겠네요 감사합니다.
그리고 현재 아이템이 내용물은 텍스트, 버튼 텍스트는 지금 wrap으로 되어있고 그걸 감싸는 linerlayout이라던지 constrainlayout 등은 match?_content로 되어있는데 내부 컨텐츠도 전부 match가 아니면 사용하는데 위험할까요?
0 추천

잘못된 부분 하나만 딱 잡자면, (디테일한 요소까지 다 언급하면 길어지므로)

holder.routine_detail.setAdapter(detailAdapter);
detailAdapter.addItems(curRoutineItem.arrayListDetail);

detailAdapter는 recyclerView 안에 recyclerView가 여러개 생겨야 하기 때문에
전역으로 선언하지 마시고, onBindViewHolder 내에서 매번 생성해서 매칭해야 합니다.

전역으로 선언해도 new로 만들면 상관없지만,
그럴 필요는 없습니다.

지금 각각 달라야 하는데 전역으로 하나만 선언해서 처리하니까,
전역 adapter에 이전에 추가한 데이터가 남아 있는데
또 추가하니까
하단이 계속 늘어나는 겁니다.

RoutineDetailAdapter detailAdapter = new RoutineDetailAdapter();
holder.routine_detail.setAdapter(detailAdapter);
detailAdapter.addItems(curRoutineItem.arrayListDetail);

저 한줄만 추가하면 동작할 겁니다.
다만, detailAdapter 관련해서
위의 전역 변수와 생성자에서 new 한 부분은 필요없으니까 삭제하세요,

마음같아서는 다 뜯어 고치고 싶습니다.
메쏘드명이 set의 성향인데 add로 하셨고
ViewHolder Class에서 setter getter 함수를 넣으실 거면 다 그렇게 써야 하는데
adapter는 또 불러서 쓰셨네요..

아무튼, 동작하면 한번 함수명칭들을 리팩토링하시기 바랍니다.
그리고 잘되어 있는 샘플들을 확인하시고 차이점을 발견하면
어느게 나은지 보고 개선하시면 깔끔하게 마무리 될 겁니다.

그런 부분은 스스로 느껴야 업그레이드 되는 부분이라,
일일이 이야기한다고 이해가 가지 않으면, 아무 의미가 없죠.

 

Will Kim (43,170 포인트) 님이 2021년 1월 19일 답변
Will Kim님이 2021년 1월 19일 수정
선생님 onCreateViewHolder에서 detailAdapter를 새로 생성하고 setAdapter해주어
각 ViewHolder마다 넣어주는건 어떤가요? setAdapter는 저의 어줍잖은 지식으로 오직  최초 1회만 실행시켜주는게 좋다하여 이렇게 생각했는데, onBindViewHolder에 넣는다면 스크롤될때마다 데이터가 바인딩 될때마다 호출될것이라 어댑터도 새로 생성되고 setAdapter도 새로 생성되는 반면, onCreateViewHolder는 상위 바깥 리사이클러뷰가 생성될때 한번만 호출되기때문에 딱 한번만 호출된다는점이 딱 맞는다고 생각하는데 어떤가요?

--
그리고 add말씀은 addItems 말씀이시죠? this.item = items를 ArrayList에 set한다고 봐야할까요? addItems가 되려면 아예 items.add() 를 썼어야 addItems라는 메소드명에 적절했을까요?
아마  will kim님은 님의 화면을 보고 nested recyclerview라고 생각하신 것 같고, codeslave 님의 화면은 독립적인 리사이클러뷰 2개인 것 같네요. 리사이클러뷰 안에 리사이클러뷰가 들어간 경우라면 will kim님 말씀대로 하셔야 하고, 독립적인 리사이클러뷰가 두개 쓰이는 경우라면 codeslave 님 접근방법이 더 효율적인 것 같다는게 제 생각입니다.
nested recyclerview가 따로있나요..? 독립적인 리사이클러뷰라는게 잘이해가 안갑니다..
리사이클로뷰 안에서 또 리사이클러뷰가 있는구조이기는 합니다..
그러니까 사진에서 운동명 세트추가 세트삭제가 하나의 아이템이고그 밑으로
TEST 무게 횟수 아이템이 포함된 구조입니다
@codeslave
티바로우랑 렛풀 다운이 상위 리사이클러 뷰이고요.
그 하단에 테스트 목록이 리사이클러 뷰입니다.
티바로우의  하단의 테스트 목록과 렛풀다운 하단의 테스트 목록은 서로 다른 거라고 판단했습니다.
그렇다면, onBindViewHoder에서
티바로우의 테스트 목록 adapter를 생성해야 하고요,
렛풀다운의 테스트 목록 adapter를 생성해야 합니다.
그래야 서로 다른 테스트 목록을 가지게 됩니다.

말씀하신 onCreate에서 한번 하는 것은
티바로우랑 렛풀다운이 같은 adapter를 가지게 되는 결과를 초래합니다.

public void addItems(ArrayList<RoutineDetailModel> items) {
        this.items = items;
        notifyDataSetChanged();
    }

위의 함수명은 addItems 이지만, 실제 코드는
this.items = items;
이므로 setItems라고 하는게 맞다고 생각합니다.

@spark 질문과 코드와 화면을 보고
리싸이클러 안에 리싸이클러 두개가 들어간 것으로 보았습니다. ^^*
말이라는게 정확히 이해되기 어려운 부분이 있지요.
코드는 말보다는 정확한 것 같습니다. ^^;
...