태그 보관물: ProgressDialog

AsyncTask와 같이 사용하는 ProgressDialog를 사용해서 태스크를 바로 종료시키기

안드로이드 애플리케이션이 데이터를 로딩하는데 AsyncTask를 사용하고, 사용자에게 데이터 로딩을 알려주려고 ProgressDialog를 같이 사용하는 경우를 종종 볼 수 있다. 아래의 그림이 이 예제이다.

위 그림의 상황에서, 사용자가 다른 화면을 보길 원하거나 더는 로딩하기 원하지 않는 경우에도 ProgressDialog 클래스의 cancelable 변수를 false로 설정해서 백 키를 눌러도 데이터 로딩창을 종료하지 않고 계속 보이는 경우를 볼 수 있다. UX의 관점에서 보면, 이런 경우는 좋지 않다.

같은 상황에서 iOS 애플리케이션들의 UX를 보니, 많은 경우 화면을 전환하기 전에 데이터를 로딩하고 로딩을 완료하면 화면을 전환한다. 이 UX로 인해서 사용자는 로딩을 취소하고 쉽게 다른 화면으로 전환할 수 있다. 물론 꼭 그렇지는 않지만, 필자의 경우에는 비교적 iOS의 UX가 안드로이드보다 좋게 느껴진다. 그래서 안드로이드 애플리케이션도 ProgressDialog 창을 종료시키고, 데이터 로딩과 같은 비동기 요청(AsyncTask를 사용하는 경우)을 바로 취소하는 방법을 사용해서 비교적 UX를 개선할 수 있다. 사용자에게 조금 더 좋은 UX를 제공할 수 있는 이 방법을 살펴보자.

1. 백 키를 사용해서 종료하는 ProgressDialog 객체 생성

		pd = new ProgressDialog(this);
		pd.setTitle("");		
		pd.setMessage("Loading...");
		pd.setCancelable(true);
		pd.setOnDismissListener(new OnDismissListener() {
			@Override
			public void onDismiss(DialogInterface dialog) {
				Log.d(TAG, "onDismissed() ");
				
				task.cancel(true);
				//task.cancel(false);
			}
		});

이 코드는 ProgressDialog 객체를 생성하면서 백 키로 창을 종료하면서 Dismiss 이벤트를 받아서 비동기 데이터를 로딩하는 태스크인 task를 취소한다.

2. 비동기로 데이터를 로딩하는 AsyncTask 클래스

	static class LoadingTask extends AsyncTask<String, Integer, Boolean> {
		private ProgressDialog pd = null;
		
		public LoadingTask(ProgressDialog pd) {
			this.pd = pd;
		}
		
		@Override
		protected void onPreExecute() {	
			super.onPreExecute();
			if(pd != null)
				pd.show();
		}
		
		@Override
		protected Boolean doInBackground(String... params) {		
			try {
				Thread.sleep(1000 * 20);
			} catch(InterruptedException e) {
				Log.d(TAG, "Exception : " + e.getLocalizedMessage());
			}
			
			return Boolean.TRUE;
		}

		@Override
		protected void onCancelled(Boolean result) {
			Log.d(TAG, "onCancelled : " + result);
			
			if(pd != null)
				pd.dismiss();
		}
		
		@Override
		protected void onPostExecute(Boolean result) {
			Log.d(TAG, "onPostExecute : " + result);
			
			if(pd != null)
				pd.dismiss();
		}
	}

이 클래스는 비동기로 데이터를 로딩하는 형태로 비동기의 태스크 로직 메서드에서 20초를 Sleep하고 있다. 그리고 이 태스크의 취소에 대한 콜백 메서드인 onCancelled(Boolean result) 메서드를 확인할 수 있다.

이제 위에서 살펴본 코드를 사용해서 사용자가 백 키를 누른 경우, ProgressDialog를 종료하고 LoadingTask도 종료시킬 수 있다. AsyncTask를 종료하는 방법으로 cancel(boolean mayInterruptIfRunning) 메서드를 호출한다. 이 메서드를 호출하면 내부에서 cancelled 변수를 true로 설정한다. 그리고 mayInterruptIfRunning를 true로 호출하면 바로 종료시키기 위한 인터럽트를 던지게 된다. 그래서 AsyncTask가 IO를 looping하면서 읽어들이지 않는 경우에는 cancel(true)로 바로 종료시킬 수 있다.

다음으로 ProgressDialog가 종료하면서 AsyncTask를 취소하는 메서드의 호출 결과를 살펴보자.
– 아래는 cancel(false)을 호출한 결과이다.

01-30 05:56:05.720: D/MainActivity(1416): onDismissed() 
01-30 05:56:24.136: D/MainActivity(1416): onCancelled : true

이 결과로 cancel(false)을 호출하면, doInBackground()가 종료된 후에 onCancelled가 호출된 것을 알 수 있다.

– 아래는 cancel(true)을 호출한 결과이다.

01-30 05:58:33.038: D/MainActivity(1584): onDismissed()
01-30 05:58:33.038: D/MainActivity(1584): Exception : null
01-30 05:58:33.038: D/MainActivity(1584): onCancelled : true

cancel(true)을 호출 결과로 AsyncTask의 doInBackground() 메서드에서 InterruptedException이 발생하게 되고, 이것으로 바로 태스크 로직을 빠져나오게 한다.

이상 살펴본 형태로 AsyncTask와 ProgressDialog를 결합해서 사용하는 경우에 사용자의 반응에 즉시 반응할 수 있는 형태로 개발해서 UX를 개선할 수 있다.

Android Dialog를 안전하고 간단하게 종료하기..

안드로이드 애플리케이션을 개발하다 보면, Dialog 부류의 위젯을 많이 사용하게 된다. 그리고 이 위젯을 화면에 보여주고 완료하면 dismiss()를 호출해서 종료를 시킨다. 이 과정에서 java.lang.NullPointerException이 발생할 수 있다. 그리고 Activty가 Dialog보다 먼저 종료(finish() 호출되는 등)가 되는 상황에서는 아래의 예외를 발생시킨다. java.lang.IllegalArgumentException: View not attached to window manager.

아래는 API 문서의 구조로, Dialog 부류의 최상위는 DialogInterface라는 것을 알 수 있다.

위 구조에서 dismiss()를 가지고 있는 클래스는 Dialog로, 이 클래스의 구조도는 다음과 같다.

ProgressDialog를 사용하면서 dismiss()를 하는 일반적이 코드는 다음과 같다.

if(progressdialog != null && progressdialog.isShowing())
{
   progressdialog.dismiss();
}

이 코드는 대체로 동작을 하지만, 위에서 언급한 예외가 발생할 수 있기에, 위의 코드를 try ~ catch로 감싸는 코드를 종종 보게 된다. 그래서 dismiss()를 안전하고 verbose하지 않게 처리하기 위해서 아래와 같은 간단한 유틸 클래스를 사용하면 좋다.

public class DialogDismisser {
	public static void dismiss(DialogInterface d) {
		if(d == null)
			return;
		
		try {
			if(d instanceof AlertDialog) {
				if(((AlertDialog) d).isShowing())
					((AlertDialog)d).dismiss();
				
				return;
			}
					
			if(d instanceof ProgressDialog) {
				if(((ProgressDialog) d).isShowing())
					((ProgressDialog)d).dismiss();
				
				return;
			}
			
			if(d instanceof Dialog) {
				if(((Dialog) d).isShowing())
					((Dialog)d).dismiss();
				
				return;
			}
		} catch(Exception e) {
			Log.e("dissmiss error", e);
		}
	}

	public static void dismiss(DialogInterface d1, DialogInterface d2) {
		dismiss(d1);
		dismiss(d2);
	}
}

이 클래스를 사용해서 각종 Dialog의 dismiss()를 안전하게 종료시킬 수 있다. 혹시 cancel()을 사용한다면 이 클래스와 동일한 형태로 작성해서 사용하면 된다.