안드로이드 개발은 쉬우면서도 어렵다.


이게 무슨 개소리냐면 머리로 구상한대로만 진행된다면 정말 그 어떤 코딩보다도 쉬운데


구상한대로 진행하다보면 꼭 생각지도 못한 문제가 발생한다.


이번에 진행할 Listview Checkbox 스크롤 문제도 그렇다.


나는 따로 Checkbox 값을 받아와야 하는 상황이 아니고, 체크박스에 체크만 하면 되기때문에 5분정도면 간단하게 구현할수 있을거라 생각했는데


Checkbox에 체크를 하고, 스크롤을 내렸다 올리면 다른 Checkbox에 체크되어있고, 원래 체크했던 Checkbox에는 체크가 지워지는 기이한 현상이 발생했다.



인터넷을 검색해보니, 자원을 아끼기 위해서 리스트뷰의 재사용 문제때문이란다.


리스트뷰에 총 30개의 row_item 이 있다고 하고, 한 화면에 10개가 출력된다고 할때, 리스트뷰는 한번에 30개를 로딩시켜놓는것이 아니라


대략 12개정도만 로딩시켜놓고, 화면을 스크롤하면 이전것들을 없앤 후에 12개째부터 24개까지 로딩시키는 방식을 사용한다고 한다.



이 글을 검색해서 들어오는 대부분의 사람들은 원인보다는 해결방안을 원할것이기 때문에 바로 해결방법에 대해 설명하도록 하겠습니다.




preparation_listview_item.xml => 리스트뷰에 들어갈 아이템

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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:orientation="horizontal">
        <TextView
            android:id="@+id/item_text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="준비물"
            android:textSize="30dp"
            android:layout_weight="2" />
        <CheckBox
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="8"
            android:gravity="center|right"
            android:id="@+id/checkbox"/>
    </LinearLayout>
</LinearLayout>
 
cs


fragment_preparation.xml => 리스트뷰가 있는 메인화면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="준비물을 확인해보세요!" />
    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </ListView>
</LinearLayout>
cs

preparation_item.java => 리스트뷰 아이템과 매칭시켜줄 데이터 객체

(저는 사진과 같이 체크박스 1개와 텍스트 1개로 이루어져 있어 간단하게 만들었고, 여러분은 본인 상태에 따라 이미지나 여러가지 변수를 추가하세요.)


1
2
3
4
5
6
7
8
9
10
11
public class Preparation_Item {
    boolean checked;
    String ItemString;
    Preparation_Item(boolean b, String t){
        checked = b;
        ItemString = t;
    }
    public boolean isChecked(){
        return checked;
    }
}
cs

** 스크롤 후 체크 이상현상을 해결하기 위해서는 데이터 객체에 반드시 boolean checked 값을 저장하고 있어야 합니다 **


arrays.xml => res/values/arrays.xml 생성 후 리스트뷰 아이템에 들어갈 값 저장.


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
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="restext">
        <item>"여권"</item>
        <item>"여권사본"</item>
        <item>"여권사진"</item>
        <item>"체크카드"</item>
        <item>"환전한 돈"</item>
        <item>"한국 돈"</item>
        <item>"지갑"</item>
        <item>"동전지갑"</item>
        <item>"여행용 책"</item>
        <item>"옷"</item>
        <item>"모자"</item>
        <item>"보조배터리"</item>
        <item>"휴대폰 충전기"</item>
        <item>"이어폰"</item>
        <item>"돼지코"</item>
        <item>"선크림"</item>
        <item>"선글라스"</item>
        <item>"라면"</item>
        <item>"볼펜"</item>
        <item>"수첩"</item>
        <item>"비상약"</item>
        <item>"바우처"</item>
        <item>"패스"</item>
        <item>"빨래 지퍼백"</item>
        <item>"슬리퍼"</item>
        <item>"세면도구"</item>
        <item>"여행용 티슈"</item>
        <item>"멀티어댑터"</item>
        <item>"셀카봉"</item>
        <item>"예비안경"</item>
        <item>"안경닦이"</item>
        <item>"렌즈"</item>
        <item>"물병"</item>
        <item>"휴대용 가방"</item>
    </array>
</resources>
 
cs


PreparationFragment.java

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
31
32
public class PreparationFragment extends Fragment {
    ListView listView;
    ArrayList<Preparation_Item> items;
    public PreparationFragment() {
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragment_preparation, container, false);
        initItems();
        listView = v.findViewById(R.id.listView);
        Preparation_Adapter mAdapter = new Preparation_Adapter(items);
        listView.setAdapter(mAdapter);
        return v;
    }
// arrays.xml 값을 불러와 Preparation_Item 객체에 저장하는 함수. 처음에는 체크박스에 체크가 되어있지 않기때문에 boolean은 false를 저장한다.
    private void initItems(){
        items = new ArrayList<Preparation_Item>();
        TypedArray arrayText = getResources().obtainTypedArray(R.array.restext);
        for(int i=0; i<arrayText.length(); i++){
            String s = arrayText.getString(i);
            boolean b = false;
            Preparation_Item item = new Preparation_Item(b, s);
            items.add(item);
        }
        arrayText.recycle();
    }
}
 
cs


Preparation_Adapter.java

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Preparation_Adapter extends BaseAdapter {
    private ArrayList<Preparation_Item> list;
// 어댑터 생성시 PreparationFragment.java 에서 만들었던 데이터 객체 리스트를 초기화
    Preparation_Adapter(ArrayList<Preparation_Item> i){
        list = i;
    }
    @Override
    public int getCount() {
        return list.size();
    }
    @Override
    public Object getItem(int position) {
        return list.get(position);
    }
    @Override
    public long getItemId(int position) {
        return 0;
    }
    public boolean isChecked(int position) {
        return list.get(position).checked;
    }
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        Context context = parent.getContext();
        if (convertView == null) {
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.preparation_listview_item, parent, false);
        }
        TextView tv_preparation = convertView.findViewById(R.id.item_text);
        CheckBox checkBox = convertView.findViewById(R.id.checkbox);
        checkBox.setChecked(list.get(position).checked);
        tv_preparation.setText(list.get(position).ItemString);
        checkBox.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view){
                boolean newState = !list.get(position).isChecked();
                list.get(position).checked = newState;
            }
        });
        checkBox.setChecked(isChecked(position));
        return convertView;
    }
}
 
cs


빨간색으로 표현한 부분이 이번 코드의 가장 핵심포인트입니다.


리스트뷰에서 체크박스를 클릭하면, 체크박스의 setOnClickListener를 호출하여 클릭한 position의 preparation_item.java 객체 boolean 함수에

!list.get(position).isChecked(); check 였다면 => uncheck, uncheck 였다면 => check 로 바꿔서 저장시키고,


checkBox.setChecked(isChecked(position)); 그 값을 checkbox에 setChecked 하여 스크롤 후에도 체크된 것들이 그대로 남아있게 됩니다.


이 문제를 해결하기 위해서 구글을 어지간히 검색해봤는데, 대부분 재사용 현상때문이다, 해결하기 위해서는 boolean값을 따로 저장하고 있어야 한다.

정도의 답변밖에 찾질 못했습니다. 물론 맞는 정답이기는 한데, 저같은 초보들은 이런식으로 코드가 있어야 확실히 이해하기 편하거든요.


앞으로도 코딩하면서 제가 여기저기 검색해서 구현 완료된 내용들에 대해서는 이런식으로 정리해서 올려드리도록 하겠습니다.


이해안가시는 부분이 있으시면 질문남겨주세요!