태그 보관물: Adapter

ListView에서 java.lang.IllegalStateException 에러 처리

서비스 하는 앱 중의 하나가 크래시를 리포팅해서 확인해보니, 다음과 같다. 이 앱에서 리포팅한 에러는 아래와 같다. 그리고 이 에러에서 보듯이 리스트뷰(ListView)를 사용하고 있다.

12-12 13:27:04.662: E/AndroidRuntime(15705): java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. Make sure your adapter calls notifyDataSetChanged() when its content changes. [in ListView(2131230758, class android.widget.ListView) with Adapter(class com.xxx.xxx.XXXAdapter)]

구글님께 문의를 한 결과, 화면에 리스트뷰가 보이는 상황에서, 데이터를 추가하는 경우에 발생할 수 있고 아래와 같이 해결하라고 한다.

listview.add(item);
 adapter.notifyDataSetChanged();

or

adapter.add(item)

하지만 같은 에러가 계속 발생했다. 그래서 에러를 트레이스 해보니, 리스트뷰에 있는 layoutChildren() 메서드(리시트뷰 노드가 가지고 있는 하위 트리를 다시 그리는)에서 예외를 발생시킨다. 이 소스에서 예외를 발생시키는 부분을 확인해 보니 다음과 같다.

// Handle the empty set by removing all views that are visible
 // and calling it a day
 if (mItemCount == 0) {
 resetList();
 invokeOnItemScrollListener();
 return;
 } else if (mItemCount != mAdapter.getCount()) {
 throw new IllegalStateException("The content of the adapter has changed but "
 + "ListView did not receive a notification. Make sure the content of "
 + "your adapter is not modified from a background thread, but only from "
 + "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
 + "when its content changes. [in ListView(" + getId() + ", " + getClass()
 + ") with Adapter(" + mAdapter.getClass() + ")]");
 }

이 소스로 mItemCount와 mAdapter.getCount()가 다르면 예외를 발생시키는 것을 알 수 있다. 즉, 위 구글님의 검색결과의 해결책은 맞는 답이다. 그런데 내 경우에는 계속 문제가 발생했다. 디버깅을 해보니, Adapter의 getCount() 메서드가 넘겨주는 값이 다른 값을 넘겨주는 경우가 발생했고 그래서 이 에러가 발생하는 것이었다. 리스트뷰의 스크롤과 클릭 이벤트를 리스너로 등록해서 이벤트가 발생하는 경우에 UI를 처리하기 위해서 UI 스레드로 처리하는 부분들이 있는데 이 영향으로 그런 것도 같다(정확하게 디버깅을 하지 않아서 추측임).

그래서, 내가 사용하는 리스트뷰는 동적으로 데이터를 입력하거나 삭제하지 않는 경우라서, 위 소스를 기준으로 Adapter클래스에서 데이터의 개수를 미리 정해놓고, getCount() 메서드는 정해진 값을 넘겨주도록 했다. 아래가 수정한 소스이다.

public XXXAdapter(Context ctx, int resource, List<Item> items) {
  super(ctx, resource, items);
  this.ctx = ctx;
  this.count = items.size();
  this.inflater = LayoutInflater.from(ctx);
}

@Override
public int getCount() {
  return count;
}

이 소스로 수정한 뒤에 이 에러는 발생하지 않는다.

ListView UI 반응성을 높이기 위한 Adapter 코드

리스트뷰(ListView)는 안드로이드에서 가장 많이 사용하는 위젯(Widget)중에 하나이다. 리스트뷰는 기본적으로 Adater를 사용해서 UI를 그리기 때문에, 일반적으로 리스트뷰는 개별 Adapter를 사용한다. 아래에서 리스트뷰가 Adater에서 어떻게 UI를 그리고 있는지 살펴보고, 다음으로 리스트뷰를 빠르게 그리기 위한 방안을 살펴보자. 아래는 예제코드에서 사용하는 Adapter의 getView 메서드 코드이다.

1. 기본
1.1 이 코드는 화면에 row(position에 해당)가 보일 때마다 컴포넌트를 만들어서 데이터를 바인딩하는 과정을 거치기 때문에 제일 느린 방법이다.

@Override
	public View getView(int position, View cView, ViewGroup pView) {
		cView = inflater.inflate(R.layout.activity_index_item, null);
		ImageView logoView  = (ImageView) cView.findViewById(R.id.activity_index_logoview);
        TextView nameView = (TextView) cView.findViewById(R.id.activity_index_txtview);

		final IndexModel model = getItem(position);
		logoView.setImageResource(model.id);
		nameView.setText(model.name);

		return cView;
	}

2. ViewHolder 패턴 사용
2.1 ViewHolder 패턴은 화면에 row가 보일 때, 전 화면에서 사용했던 View(cView)를 재사용할 수 있는 경우 사용해서 빠른 UI를 만들어 낸다.
2.2 단점은 체크박스나 ImageView등을 동적(네트웍으로 썸네일을 가져오는 등)으로 처리해야 하는 경우, 깜박이는 효과(?)를 발생시킨다.

	
	@Override
	public View getView(int position, View cView, ViewGroup pView) {
		IndexViewHolder holder;

		if (cView == null) {
			holder = new IndexViewHolder();

			cView = inflater.inflate(R.layout.activity_index_item, null);
			holder.logo = (ImageView) cView.findViewById(R.id.activity_index_logoview);
			holder.name = (TextView) cView.findViewById(R.id.activity_index_txtview);
		} else {
			holder = (IndexViewHolder) cView.getTag();
		}

		cView.setTag(holder);

		final IndexModel model = getItem(position);
		holder.logo.setImageResource(model.id);
		holder.name.setText(model.name);

		return cView;
	}

3. 제시방안
3.1 위의 ViewHolder 패턴과 거의 비슷한 성능과, ViewHolder 패턴이 가지고 있는 단점을 해결한다.
3.2 로직
3.2.1. SparseArray에 position별로 getView에서 리턴하는 View를 유지한다.
3.2.2. getView에서 position에 해당하는 View가 SparseArray에 있으면 View를 리턴한다.
3.2.3. 많은 View를 가지면 메모리 부담이 있기에, WeakReference로 View를 래핑한다.

	
package net.sjava.ex.sparseview;

import java.lang.ref.WeakReference;
import java.util.List;

import android.content.Context;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import static android.content.Context.LAYOUT_INFLATER_SERVICE;

public class IndexAdapter extends ArrayAdapter<IndexModel> {
	private LayoutInflater inflater;
	private SparseArray<WeakReference<View>> viewArray;

	public IndexAdapter(Context ctx, int txtViewId, List<IndexModel> models) {
		super(ctx, txtViewId, models);
		this.viewArray = new SparseArray<WeakReference<View>>(models.size());
		this.inflater = (LayoutInflater) ctx.getSystemService(LAYOUT_INFLATER_SERVICE);
	}

	@Override
	public View getView(int position, View cView, ViewGroup pView) {
		if(viewArray != null && viewArray.get(position) != null) {
			cView = viewArray.get(position).get();
            if(cView != null)
			    return cView;
		}

		try {
			cView = inflater.inflate(R.layout.activity_index_item, null);
			ImageView logoView = (ImageView) cView.findViewById(R.id.activity_index_logoview);
			TextView nameView = (TextView) cView.findViewById(R.id.activity_index_txtview);

			final IndexModel model = getItem(position);
			logoView.setImageResource(model.id);
			nameView.setText(model.name);
		} finally {
			viewArray.put(position, new WeakReference<View>(cView));
		}
		return cView;
	}

	@Override
	public void notifyDataSetChanged() {
		super.notifyDataSetChanged();
	}

	public void update() {
		viewArray.clear();
		notifyDataSetChanged();
	}
}

위 update 메서드는 Activity에서 리스트 아이템의 추가, 삭제, 수정등의 액션이 일어나면, 반영하기 위해서 추가했다. 이 코드는 View를 Holder로 재사용하는 방식이 아니기 때문에, 체크박스나 썸네일 이미지가 깜박이는 현상이 없고, View를 SparseArray가 유지하기에, ViewHolder 패턴만큼의 속도를 보여줄 수 있겠다. 이상으로, 썸네일 캐시처럼, View를 캐시해서 ListView 스크롤의 UI 반응성을 높이는 Adapter 코드이다.

* 예제코드
cfile30.uf.216A7244521290750F6D3C.zip