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

동적 중첩 리사이클러뷰 아이템 추가시 랜덤변경(?) 문제

0 추천

RoutineModel.java (부모)

public class RoutineModel {
    private List<RoutineDetailModel> routineDetailsList;
    private String routine;

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

    public List<RoutineDetailModel> getRoutineDetailsModel() {
        return routineDetailsList;
    }

    public void addDetails(RoutineDetailModel item) {
        if(routineDetailsList == null) {
            routineDetailsList = new ArrayList<>();
        }
        this.routineDetailsList.add(item);
    }
    public boolean removeDetails(int index) {
        if(routineDetailsList == null || index >= routineDetailsList.size() || index < 0) return false;
        this.routineDetailsList.remove(index);
        return true;
    }

    public String getRoutine() {
        return routine;
    }


    public int getDetailsSize() {
        if(routineDetailsList == null) return 0;
        return routineDetailsList.size();
    }
}

 

MultipleAdapter.java ( 어댑터)

public class MultipleViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
    private final Context context;
    private List<Object> items;
    private OnItemClickListener onItemClickListener;

    public MultipleViewAdapter(Context context, List<Object> items) {
        this.context = context;
        this.items = items;
    }

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


    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        Object object = items.get(position); //Object or Generic class or abstarct base Model class parent of Routine and RoutineDetails>
        
        if(object instanceof RoutineModel) {
            updateRoutineViews((RoutineViewHolder) holder, (RoutineModel) object, position);
        } else if(object instanceof RoutineDetailModel) {
            updateRoutineDetailsViewHolder((RoutineDetailsViewHolder) holder, (RoutineDetailModel) object, position);
        }
    }

    private void updateRoutineViews(RoutineViewHolder holder, RoutineModel routineItem, int position){
        holder.routine.setText("Routine " + routineItem.getRoutine());
        
        holder.addSet.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(onItemClickListener != null) onItemClickListener.onClick(v, position);
            }
        });

        holder.deleteSet.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(onItemClickListener != null) onItemClickListener.onClick(v, position);
            }
        });
    }
    @Override
    public int getItemViewType(int position) {
        Object object = items.get(position);
        if(object instanceof RoutineModel){
            return 1;
        }
        //else if instanceOf RoutineDetailModel return 0
        return 0;
    }

    @Override
    public int getItemCount() {
        if(items == null) return 0;
        return items.size();
    }

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

    public void swapData(List<Object> newItems) {
        if (newItems != null) {
            this.items = newItems;
            notifyDataSetChanged();
        }
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public interface OnItemClickListener{
        void onClick(View view, int position);
        void onLongClick(View view, int position);
    }


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

        public RoutineViewHolder(@NonNull View itemView) {
            super(itemView);
            //initViews(); in constructor
            routine = itemView.findViewById(R.id.routine);
            addSet = itemView.findViewById(R.id.add_set);
            deleteSet = itemView.findViewById(R.id.delete_set);
        }
    }

    public static class RoutineDetailsViewHolder extends RecyclerView.ViewHolder {
        
    }
}

 

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private MultipleViewAdapter adapter;
    private List<RoutineModel> routineList;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        contentList.setLayoutManager(new LinearLayoutManager(this));
        adapter = new MultipleViewAdapter(this, new ArrayList<>());
        contentList.setAdapter(adapter);

        adapter.setOnItemClickListener(new MultipleViewAdapter.OnItemClickListener() {
            @Override
            public void onClick(View view, int position) {
                Object item = (Object) adapter.getItem(position);
                if(item instanceof RoutineModel) {
                    RoutineModel routineModel = (RoutineModel) item;
                    if (view.getId() == R.id.add_set) {
                        int weight = randomInt(99);
                        routineModel.addDetails(new RoutineDetailModel(routineModel.getDetailsSize() + 1, weight));
                        adapter.swapData(getMixedList()); // OR add item to adapter and notify item inserted

                    } else if (view.getId() == R.id.delete_set) {

                        boolean deleted = routineModel.removeDetails(routineModel.getDetailsSize() - 1); // -1 !!! to delete last item
                        adapter.swapData(getMixedList()); // OR remove item from adapter and notify item removed

                    }

                }
            }

            @Override
            public void onLongClick(View view, int position) {
                //empty
            }
        });

        initFakeData();
        adapter.swapData(getMixedList());
    }

    private List<Object> getMixedList() {
        List<Object> mixedList = new ArrayList<>();
        for(RoutineModel rm: routineList){
            mixedList.add(rm);
            if(rm.getRoutineDetailsModel() != null && rm.getRoutineDetailsModel().size() > 0){
                for(RoutineDetailModel rmdetilas: rm.getRoutineDetailsModel()){
                    mixedList.add(rmdetilas);
                }
            }
        }
        return mixedList;
    }


    private void initFakeData() {
        routineList = new ArrayList<>();
        for(int i = 0; i < 5; i++){
            RoutineModel routineModel = new RoutineModel(String.valueOf(i + 1));
            for(int j = 0; j < 4; j++){
                routineModel.addDetails(new RoutineDetailModel(j+1, randomInt(99)));
            }
            routineList.add(routineModel);
        }
    }

    private int randomInt(int max) {
        return (int) Math.floor(Math.random() * max);
    }
}
codeslave (3,940 포인트) 님이 2021년 1월 26일 질문
codeslave님이 2021년 1월 28일 수정

2개의 답변

0 추천
제가 보기에는 Nested RecyclerView를 쓰지 않아도 될 것처럼 보입니다만.  RecylcerView한 개에 Routine별로 헤더를 달면 되는 걸로 보이네요. 변경된 아이템만 갱신하기 위해서 DiffUtil이나 DiffUtil이 구현된 ListAdapter를 쓰시면 될 것 같구요. 동적으로 RecyclerView를 추가하는 경우는 본 적이 없는 것 같아요. 그렇게 하려면 고려해야할 요소들이 많아져서 너무 복잡해 지거든요. 예를 들면 View pool의 갯수 조정, RecyclerView 간에 뷰공유 등등, 좀 어려운 영역을 건드려야 하기 때문에, 저라면 순수 공부하는 목적이 아니라면 그냥 RecyclerView  하나로 구현을 할 것 같습니다. Adapter에 화면에 필요한 데이터만 제공해 주면 될 것 같아요.
spark (227,830 포인트) 님이 2021년 1월 26일 답변
흠 너무 어려운 방법 or 비효율적인 방법으로 시도하고있었던건가요..
딱 보자마자 루틴리스트 안에 또다른 싱세 리스트가 있기에 리사이클러뷰의 리사이클러뷰를 사용해야된다고 생각했었는데..
DiffUtill이라는것은 찾아봤는데 대강 제가 원하는 딱 변경된 아이템만 변경해주고
notifydatasetchanged를 사용안해 비효율을 막는다? 라는 것 같네요..

그런데 헤더라는것은 정확하게 무엇인지 감이안잡히는데 검색해봐도 header / footer
정도만 나오는것같은데 이거 말씀 맞으실까요..?
0 추천

음.. 제가 보기엔 개념이 헷갈리는 상태입니다.

아래 동영상을 보시면, (제가 만들라다가 시간이 없어서)
https://www.youtube.com/watch?v=EyUjw6b5gXE

그래서 강좌를 보면 이분은 소스공개를 해 놨는데요, 소스를 보면

public class Item {
    private String itemTitle;
    private List<SubItem> subItemList;
...

위에처럼, 데이터 구조를 클래스로 정의했죠.
클래스 Item의 목록은 부모 recyclerview로
SubItem 클래스의 목록은 부모 하나마다 존재하니까,
N개가 존재하는 겁니다.
당연히 child recyclerview도 N개가 생성되고요.

그래서 강좌의 소스를 좀 더 자세히 디버깅까지 해가면서 이해하시기 바랍니다.

결론적으로 리사이클러뷰에서 additem을 만들어서 외부에서 제어하긴 힘들어요.
(불가능하진 않지만, 구현한다고해도 성능이 떨어집니다.)

데이터를 바꾸어서 다시 그리는 개념이라고 생각하면 됩니다.

즉 원본 데이터를 바꾸어 다시 던지면 그 바뀐 원본 데이터에 맞게
그리는 역할만 하는 것이죠.


아래는 부모 리사이클러뷰의 onBindViewHolder입니다.
SubItemAdapter를 생성해서 매칭하죠?
부모가 N개면, N개의 리사이클러뷰가 생성이되고,
데이터에서 item 하나하나마다 다르게 있는 subitem을 각각
리사이클러뷰에 던지는 겁니다.

@Override
    public void onBindViewHolder(@NonNull ItemViewHolder itemViewHolder, int i) {
        Item item = itemList.get(i);
        itemViewHolder.tvItemTitle.setText(item.getItemTitle());

        // Create layout manager with initial prefetch item count
        LinearLayoutManager layoutManager = new LinearLayoutManager(
                itemViewHolder.rvSubItem.getContext(),
                LinearLayoutManager.VERTICAL,
                false
        );
        layoutManager.setInitialPrefetchItemCount(item.getSubItemList().size());

        // Create sub item view adapter
        SubItemAdapter subItemAdapter = new SubItemAdapter(item.getSubItemList());


        itemViewHolder.rvSubItem.setLayoutManager(layoutManager);
        itemViewHolder.rvSubItem.setAdapter(subItemAdapter);
        itemViewHolder.rvSubItem.setRecycledViewPool(viewPool);
    }

바깥에서 Add버튼을 눌렀을 때,
item - subitem 데이터 구조를 바꾸어서 던지면 다시 그리는 겁니다.

Will Kim (43,170 포인트) 님이 2021년 1월 27일 답변
선생님이 걸어주신 링크의 소스를 깃에서 받아 돌려보고 코도를 천천히 읽어봤는데요. 소스 코드 자체는 이해가 갑니다만 선생님께서 외부에서 목록안의 목록을 추가하는 것에 대한것이 잘 이해가 안갑니다. 이 자체가 이해가 안간다기보다는 영상의 소스에서도 어댑터에서 메인액티비티는 외부니까.. 메인액티비티에서 서브 아이템의 목록을 만들고 그걸 이제 다시 부모 아이템에 넣고 이제 어댑터를 만들어주던데요.. 여기서도 그러면 이 서브 아이템을 추가하는 방식자체는 방식은 별로 좋지 못한방식인건가요?

그외에 뭐 setInitialPrefetchItemCount 라던지, RecyclerView Pool 이라던지 이런것 빼고는 코드가 이해는 갑니다..
표시하고자 하는 데이터를 외부에서 던져서 그리는 방법이 가장 심플하고요.

부모 밑에 N개의 차일드어댑터가 있다면, 어떤 차일드 어댑터를 다시 그릴 것인지 루프를 돌아서 그 차일드 어댑터를 다시 생성해야 하니까 비효율적이라고 보는 겁니다.
훔,,그럼 onBindViewHolder에서도 setAdapter를 하는것은 계속해주는것은 좋지못한 방식은 맞다 이건가여..?
onBindViewHolder에서 계속 setAdapter를 하는 방법외에 다른 방법은 없습니다.
setAdapter는 그릴때 한번 하는 것입니다.
그러나 지금 하려는 것은 그린 뒤에도
Adapter의 데이터를 변경하려는 것이잖아요.

[일반적인 방식]
초기 데이터로 어댑터를 그린다.
추가된 정보를 데이터에 반영한다.
변경된 데이터로 어댑터를 또 그린다.
(반복)

[지금 하시려는 것]
초기 데이터로 어댑터를 그린다.
추가된 데이터만 리사이클러뷰로 던진다.
수정된 데이터를 적용하는 차일드 리사이클러뷰를 찾는다
그 해당하는 차일드 리사이클러뷰의 어댑터터에 추가된 데이터를 전달한다.
전달받은 데이터로 행을 추가한다.
(반복)

제가 보내드린 예시가 좋은 방법입니다.
[지금 하시려는 방법]이 좋지 않은 방법이라고 이야기 하는 겁니다.
지금하시려는 방법으로 해도, 계속 setAdapter를 해야 하는 것이고,
변경된 차일드 리사이클러뷰가 뭔지 찾아서 그것만 수정해야 하는 것입니다.
그런 방식이 일반적인 방식도 아니고,
리사이클러뷰가 그렇게 동작하는 라이브러리가 아니라,
위에 일반적인 방식으로 동작하도록 구현된 라이브러리입니다.

이야기가 계속 길어지는데,
이중 리사이클러 뷰를 그리는 방법을 샘플로 확인 했다면,
버튼을 눌러서 추가하는 것을 리사이클러뷰에서 하지말고
데이터에 반영하고, 리사이클러뷰에 또 던지면 되는 것입니다.

질문자의 또 다른 문제점은
좋은 샘플을 찾아서 이해한 뒤에
그것을 따라서 잘 구현 하고,
테스트를 통해서 문제를 보완하면 되는데,

누군가가 어떤 문제가 있다는 내용을 인터넷에 공지하면 그것에 너무 신경을 쓴다는 것입니다.
그 사람이 그 글을 언제 썼고, 그 사람이 한 이야기가 맞다는 보장도 없습니다.

그런 문제를 미연에 방지하는 방법은
리사이클러뷰 샘플 중에서 최신, 그리고 가장 좋은 평점과
스택오버플로우 같은 곳에서 선택된 답변에 있는 것들을 찾아서
여러개의 샘플을 실행해보고
가장 좋아 보이는 샘플을 선정해서
그걸로 구현하는 방법을 제안합니다.

개념이 제대로 정립되지 않은 상태에서
잘못된 정보로 구현한 소스 코드를 가지고
계속 스스로 문제를 해결하려고 한다면,
시간 소모가 매우 많습니다.

그럴바에는 그 시간을 좋은 샘플을 찾는 것에 집중하고,
내가 구현하고자 하는 것과 가장 유사하고, 활용이 가능한 샘플을 찾는 데
하루 정도만 할애해도,
무수하게 많은 샘플을 컴파일하고 실행해 볼 수 있습니다.

거기서 출발하는 게 초급자들에게는 가장 빠른 방법이고,
제대로 감을 잡을 수 있는 방법입니다.

가끔씩 좋지 않은 샘플로 시작해서,
그걸 스스로 해결하려고 하다가 개발을 포기하시는 분들을 여럿 봤습니다.
그 레벨에서 해결할 수 없는 것을 시도하는 것보다는
올바른 개념을 배운다는 느낌으로 접근하시기를 바랍니다.
그것이 가장 Shortest Path입니다.
아.... 그렇군요ㅠ따끔한 말씀 감사합니다. 안그래도 느끼고 있던 참이었습니다...
예를들면 이번 setAdapter()에서 제가 어디선가 본 글에서는 `setAdapter()는 최초 1회만 호출되어야한다` 때문에 다른 중첩 리사이클러뷰의 여러 샘플 코드를 봤을때 onBindViewHolder() 에서 중첩 리사이클러뷰의 데이터를 바인딩할때 setAdapter를 해주는데 이건.. 스크롤..재활용 될때마다 onBindBiewHolder는 여러번 호출되니까 setAdaper()도 자동으로 여러번 호출될테니 이건 `틀린 코드`다 하고 너무 빙빙 돌리다 이코드 저코드 작성하다 꼬이고 꼬이고 개념이 이해가 안가고 그렇게 된것같네요..

따끔한 말씀 감사합니다ㅜ

위의 일반적인 중첩 리사이클러뷰의 코드를 보고 다시한번 정리해보도록하겠습니다.
...