티스토리 뷰

반응형
SMALL

Android 에서 Recycler View를 사용할 때 Adapter에 데이터 List를 넘겨주게 된다.

아래 처럼 말이다.

ArrayList<String> imageUrls;
StoryImageAdapter adapter = new StoryImageAdapter(context, imageUrls);

이 때 imageUrls라는 ArrayList에 데이터를 넣고 Adapter Constructor의 매개 변수로 넘겨주게 되는데 

보통은 아래처럼 데이터를 넣어 주게 된다.

public void setImageUrlSet(ArrayList<String> urls) {
    this.imageUrls = urls;
}

위의 코드는 urls라는 매개 변수 값을 받아와서 imageUrls라는 ArrayList에 대입해주는 구문인데 자세히 살펴보면 이는 Call by Value가 아닌 Call by Reference가 된다. 즉, 값이 복사되는게 아니라 레퍼런스가 같이 때문에 urls에 넣어주는 ArrayList의 값이 바뀌면 imageUrls의 값도 바뀐다는 의미이다.

 

Call By Reference를 이해하기 위한 코드를 하나 적어두겠으니 참고하기 바란다.

public class Main {

    private static ArrayList<String> source = new ArrayList<>();
    private static ArrayList<String> listA;
    private static ArrayList<String> listB = new ArrayList<>();

    public static void main(String[] args) {
        for(int i = 0; i < 10; i++) {
            source.add("test" + i);
        }

        setListA(source);
        setListB(source);

        System.out.print("Source : ");
        printArrayList(source);

        System.out.print("listA : ");
        printArrayList(listA);

        System.out.print("listB : ");
        printArrayList(listB);

        System.out.println("listA.set(0, changed)");
        listA.set(0, "changed");

        System.out.print("Source : ");
        printArrayList(source);

        System.out.print("listA : ");
        printArrayList(listA);

        System.out.print("listB : ");
        printArrayList(listB);


    }

    public static void setListA(ArrayList<String> paramList) {
        listA = paramList;
    }

    public static void setListB(ArrayList<String> paramList) {
        listB.addAll(paramList);
    }

    public static void printArrayList(ArrayList<String> list) {
        for(String i : list) {
            System.out.print(i);
            System.out.print(", ");
        }
        System.out.println("");
    }
}

Single RecyclerView라면 어차피 Activity나 Fragment에서 값을 넘겨주니 크게 상관은 없겠지만, Nested RecyclerView의 상황이라면 어떨까?

아래와 같은 코드가 있다고 생각하자.

public class StoryAdapter extends RecyclerView.Adapter {

    private List<String> urls = new ArrayList<>();
    private List<String> imageUrls;

    public StoryAdapter(Context context, List<Story> list) {
        this.context = context;
        this.list = list;
    }

    ...


    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if(holder.getAdapterPosition() != RecyclerView.NO_POSITION) {
            urls.clear();
            ...

            if(!item.getImgUrl().isEmpty())
                urls.add(item.getImgUrl());

            ...

            if(urls.size() > 0) {
                imageUrls = urls;
                ...

                StoryImageAdapter adapter = new StoryImageAdapter(context, imageUrls);
                holder.recyclerView.setAdapter(adapter);

                ...

            }
        }
    }    
}
public class StoryImageAdapter extends RecyclerView.Adapter {
    private List<String> imageUrls;

    public StoryImageAdapter(Context context, List<String> list) {
        this.context = context;
        this.imageUrls = list;
    }

    ...

    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if(holder.getAdapterPosition() != RecyclerView.NO_POSITION) {

            ...

            imageUrls.get(holder.getAdapterPosition());

            ...
            
        }
    }    
}

 

흔한 RecyclerView의 Adapter 코드이다.

여기서 이상한 점을 발견할 수 있겠는가? 참고로 위 코드대로 실행했을 때 해당 RecyclerView가 있는 Fragment로 이동하면 아래와 같은 오류가 발생한다.

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 2(offset:2).state:3

IndexOutOfBoundsException...?? 말도 안된다고 생각이 드는 에러가 발생할 것이다.

 

하지만, 코드를 잘 살펴보자.

위의 코드는 Nested(중첩된) RecyclerView이기 때문에 Adapter코드 안에 setAdapter 메소드가 있다.

 

imageUrls = urls;  이 코드를 살펴보도록 하자.

 

아까 위에서 대입은 Call by Reference라고 하였다. Call by Reference라면 값이 복사가 되는 것이 아니라 같은 객체를 참조하게 되기 때문에 엉뚱한 곳에서 값을 바꿀 수 있다는 것이다.

 

urls라는 List는 Story List의 item이 바뀔 때마다 clear하고 add를 해주고 있기 때문에 문제가 되지 않는다.

하지만 imageUrls라는 List를 살펴보면, item이 변경될 때마다 (즉, onBindViewHolder가 실행될 때 마다) 대입을 해주고 있기 때문에 얼핏 스쳐가면서 보면 문제가 안될 것이라고 생각하겠지만, StoryAdapter 클래스의 필드 변수로 선언이 되어 있다는 점에 주목해야 할 것이다.

 

StoryAdapter는 1개이지만, 그 안의 StoryImageAdapter는 Item마다 하나씩 생성이 된다.

StoryImageAdapter 클래스 안에도 역시 this.imageUrls = list; 와 같은 Constructor가 존재하고 이러면 레퍼런스로 동일한 imageUrls라는 ArrayList를 참조하게 된다는 것이다.

 

그러면, StoryAdapter에서 아이템이 변경될 때마다 imageUrls에 새로운 리스트를 참조하게 한다면, StoryImageAdapter에 있는 bindViewHolder 메소드에서 Error가 날 수 있다는 것이다.

 

왜냐하면, 우리가 사용할 데이터는 1부터 3까지인데 다른 아이템으로 인해 1부터 3까지가 1부터 2까지로 변경이 되었다고 한다면 IndexOutOfBoundsException이 발생한다는 것이다!

 

해결방법은 어떨까?  간단하다.

대입을 해주었던 코드를 이렇게 바꾸어 주면 끝이 난다.

imageUrls = new ArrayList<>();
imageUrls.addAll(urls);

 

나는 이것을 회사 프로젝트로 개발하면서 무심코 적어놓았던 것을 아무 죄없는 코드만 만지면서 1주일을 허송세월하면서 보냈다.

 

하지만 오늘 이것을 발견하면서 나는 또 하나를 배울 수 있게 되었다.

혹시나 안드로이드 개발하면서 이러한 문제를 겪는 사람이 있다면 해결할 수 있기를 바란다.

반응형
LIST
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함