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

리사이클러뷰 onBindViewHolder와 onCreateViewHolder에서 리스너설정 차이점

0 추천

https://ibb.co/QYBqvfG

사진 용량이 때문에 링크로 대체합니다.

EditText란에 데이터를 입력하고 아이템을 스크롤 될정도로 추가합니다.

이후 스크롤후에 다시 돌아오면 데이터가 지워져있습니다. 사진에서는 지워지는것만 나오지만

데이터를 추가로 입력하고 계속추가하다보면 이전에 입력했던 아이템의 데이터가 이후 추가된

아이템에 셋팅된채로 추가되는 경우도 보입니다. 스크롤하면 랜덤하계 지속적으로 바뀌구요.

 

입력한 데이터를 실시간으로 아이템으로 저장하고 다시 아이템에서 불러와 셋팅하기위해서

TextWatcher를 사용했습니다. 문제의 원인은 대충 알것같은데 해결된 원인은 잘모르겠습니다.

 

onBindViewHolder에서 뷰가 재사용되니까 입력된 데이터가 아이템에 추가된채로 재사용되어 추가되고 이런식으로 일어난 문제로 추측됩니다..

onBindViewHolder에서 TextWatcher를 설정했는데, 이것때문에 위 문제가 계속 일어나다가

onCreateViewHolder..그러니니까 뷰홀더의 생성자내에서 TextWatcher를 설정하니 해결되네요..

왜 해결이 된건가요?

 onBindViewHolder에서 리스너를 설정하고 이걸 재활용하면 무슨일이 일어나는건가요..

데이터를 실시간으로 입력받아서 아이템에 저장하고 그 저장된 아이템의 데이터를 셋팅하는건 똑같은거같은데..

 

 

해결책이 두가지 정도 있던데 왜 이렇게 되는지 설명좀 해주시면 감사하겠습니다..

해결되기전과 후 코드 둘다 올리겠습니다.

 

@Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        Object curItem;
        switch (getItemViewType(position)) {
            case TYPE_ROUTINE_DETAIL:
                curItem = getItem(position);
                ((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) {
                        ((RoutineDetailModel) curItem).setWeight(((RoutineDetailViewHolder) holder).weight.getText().toString());
                    }
                });
                ((RoutineListAdapter.RoutineDetailViewHolder) holder).bind((RoutineDetailModel) curItem);
                break;
        }
    }

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

        public RoutineDetailViewHolder(@NonNull View itemView) {
            super(itemView);
            set = itemView.findViewById(R.id.set);
            weight = itemView.findViewById(R.id.weight);
        }

        private void bind(RoutineDetailModel item) {
            set.setText(item.getSet().toString() + "set");
            weight.setText(item.getWeight()); // 스크롤 됐을때 데이터 유지하기 위함.
        }
    }

 

후 (정상 코드1)- onCreateViewHolder() (뷰홀더 생성자에 리스너 설정하기)

@Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        Object curItem;
        switch (getItemViewType(position)) {
            case TYPE_ROUTINE_DETAIL:
                curItem = getItem(position);
                ((RoutineListAdapter.RoutineDetailViewHolder) holder).bind((RoutineDetailModel) curItem);
                break;
        }
    }

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

        public RoutineDetailViewHolder(@NonNull View itemView) {
            super(itemView);
            set = itemView.findViewById(R.id.set);
            weight = itemView.findViewById(R.id.weight);

            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) {
                    RoutineDetailModel item = (RoutineDetailModel) getItem(getAdapterPosition());
                    item.setWeight(weight.getText().toString());
                }
            });
        }

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

 

후 (정상코드2) - 리스너 삭제하고 다시 리스너 추가하기

@Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        Object curItem;
        switch (getItemViewType(position)) {
            case TYPE_ROUTINE_DETAIL:
                curItem = getItem(position);
                ((RoutineListAdapter.RoutineDetailViewHolder) holder).bind((RoutineDetailModel) curItem);
                break;
        }
    }

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

        public RoutineDetailViewHolder(@NonNull View itemView) {
            super(itemView);
            set = itemView.findViewById(R.id.set);
            weight = itemView.findViewById(R.id.weight);
        }

        private void bind(RoutineDetailModel item) {
            weight.removeTextChangedListener(textWatcher);

            textWatcher = 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(weight.getText().toString());
                }
            };
            weight.addTextChangedListener(textWatcher);
            weight.setText(item.getWeight());
            set.setText(item.getSet().toString() + "set");
        }
    }

 

 

 

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

2개의 답변

0 추천

addTextChangedListener 의 안드로이드 소스코드입니다.

public void addTextChangedListener(TextWatcher watcher) {
        if (mListeners == null) {
            mListeners = new ArrayList<TextWatcher>();
        }
        mListeners.add(watcher);
    }

 

보시는 것처럼,  기존에  TextWatcher가 존재한다면 새로운 TextWatcher 를 추가합니다. 따라서 onCreateViewHolder에는 뷰의 재사용을 위해 매번 호출되지 않기 때문에  TextWatcher가 한번만 설정이 되지만, onBindViewHolder의 경우는 매번 호출되기 때문에 기존에 TextWatcher가 존재할 경우는 다른 TextWatcher를 추가하게 될겁니다.

따라서 onCreateViewHolder가 TextWatcher를 세팅하는데 맞는 이벤트라고 생각됩니다.

spark (227,470 포인트) 님이 2021년 3월 21일 답변
감사합니다. 세가지만 질문드리겠습니다

1. onCreateViewHolder()는 딱 한번만 호출되기때문에 이곳(뷰홀더 생성자)에서 리스너를 설정하면 뷰에 설정한 리스너가 계속해서 이 TextWatcher를 안고가지만,
만약 onBindViewHolder에서 리스너를 설정하게된다면, 재사용될때마다 새 TextWatcher를 만들어서 추가하기때문이란거죠?

예를들어 1번, 2번, 3번, ... ,아이템뷰가 있고 onBindViewHolder에서 리스너를 설정한다고 가정했을때,

아이템이 스크롤이 될정도로 아이템이 여럿 추가된 상황에서 스크롤을 해서
1번 아이템이 재활용 됐다고 치면,

 spark님이 올려주신 코드에서 1번 아이템의 뷰마다 가지고 있는 mListeners는 현재 TextWatcher를 하나만 가지고 있는 상태에서 onBindViewHolder에서 TextWatcher를 다시 추가했으므로
또 하나가 추가됨. 즉 1번 아이템의 mListeners가 내부적으로 TextWatcher를 두개 가지고 있다는 말인고 계속해서 이런식으로 추가된다는 말이죠?

2. 위 질문에 대해 제가 제대로 이해했다는 가정하에..
본문 제가 올린 문제의 사진을 보면 스크롤했다가 다시 돌아가면 데이터가 기본 디폴트 데이터로 돌아갑니다. 이 이유는 무엇인가요?

리스너에 TextWatcher가 하나만 가지는게 아니라 계속해서 새로 설정돼서 추가되는건 알겠는데 처음에 데이터를 입력했으면 아이템에도 저장이 되어있을건데,
왜 스크롤하고 다시 돌아오면 리셋이 되어있을까요..

3. 이 증상을 테스트하면서 느낀거지만 앱을 몇번을 실행해도 문제가 발생하기 시작하는 아이템 갯수?가 일정하더군요.. 첫아이템 추가하고 한 22~3개쯤 추가하고 충분히 스크롤할 상태가되면 문제가 발생하던데 리사이클러뷰는 재활용하는게 매번 랜덤이 아니라 정해져있나요
1. 님이 이해하신 게 맞구요.
2. 왜 그런지는 디버깅을 해보셔야할 사항 같아요. ViewHolder에 실제 바인딩하는 데이터가 어떤 데이터가 들어오는지 확인해 보세요. 올려주신 코드는 onBindViewHolder랑 ViewHolder 뿐이라서 뭐라 말씀드리기 어렵네요. ViewHollder 자체는 별 문제가 없어 보이는 거로 봐서는 외부에서 데이터를 설정할 때 문제가 있을 거라는 추측만 가능할 뿐입니다.
3. https://bignerdranch.tistory.com/61 전에 제가 간략하게 정리를 해놓았는데, 여기 보시면 ViewHolder가 언제 재활용이 되는지 확인하힐 수 있습니다. 결론적으로 RecyclerView의 ViewHolder 재활용은 일정한 갯수로 된다고 할 수 있고, 상황에 따라 원하시면 재사용을 하지 않을 수도 있고 이걸 조절할 수도 있습니다. 물론 이런 경우는 거의 없지만요.
0 추천

1. 님이 이해하신 게 맞구요.
2. 왜 그런지는 디버깅을 해보셔야할 사항 같아요. ViewHolder에 실제 바인딩하는 데이터가 어떤 데이터가 들어오는지 확인해 보세요. 올려주신 코드는 onBindViewHolder랑 ViewHolder 뿐이라서 뭐라 말씀드리기 어렵네요. 
TextWatcher를 사용하실 때 한가지 주의할 점은 bind를 호출할 때 TextWatcher에 설정해 놓았던 이벤트가 호출될 것라는 접입니다.

private void bind(RoutineDetailModel item) {
     set.setText(item.getSet().toString() + "set");
     weight.setText(item.getWeight());

     // afterTextChanged(...)  ====> 이게 호출이 되겠죠?
 }
 
@Override
 public void afterTextChanged(Editable s) {
     RoutineDetailModel item = (RoutineDetailModel) getItem(getAdapterPosition());
     item.setWeight(weight.getText().toString());
 }

               
TextWatcher를 bind할 때 지정해주셔 되는데, 이렇게 하시려면 remove를 먼저하고 add를 하시거나 TextWatch가  이미 지정되어 있을 경우는 다시 add하지 않으시는 것도 방법일 수 있겠고, 아랫처럼 포커스가 있는 경우( 즉, 사용자가 직접 입력하는 경우에 해당)에만 TextWatcher 코드가 실행되도록 하실 수도 있을 것 같습니다.

@Override
 public void afterTextChanged(Editable s) {
     if (!set.hasFocus()) return;

     RoutineDetailModel item = (RoutineDetailModel) getItem(getAdapterPosition());
     item.setWeight(weight.getText().toString());
 }


3. https://bignerdranch.tistory.com/61 전에 제가 간략하게 정리를 해놓았는데, 여기 보시면 ViewHolder가 언제 재활용이 되는지 확인하힐 수 있습니다. 결론적으로 RecyclerView의 ViewHolder 재활용은 일정한 갯수로 된다고 할 수 있고, 상황에 따라 원하시면 재사용을 하지 않을 수도 있고 이걸 조절할 수도 있습니다. 물론 이런 경우는 거의 없지만요.

spark (227,470 포인트) 님이 2021년 3월 22일 답변
spark님이 2021년 3월 22일 수정
이해가 어느정도 간것같습니다...감사합니다 올려주신 링크글도 읽어봤는데 신기하네요 감사합니다
...