ちょっと話題の記事

AsyncTaskLoaderを使ってみる

2012.06.29

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AsyncTaskLoader

こんにちは。こむろです。最近のクールでナウな非同期処理を行うためには、Loaderクラスというものが使われるようです。Android3.0(API Level 11)から導入されている非同期処理を行うクラスです。サブクラスとしてAsyncTaskLoaderやCursorLoaderが定義されています

Loaderクラスは、SupportPackageにも入っていますのでどのVersionでも利用が可能です。今回は今までAsyncTaskで行っていた処理をAsyncTaskLoaderで実装しなおし、その違いを見てみます

リニューアルされたリファレンスページには、Loader関係の情報はここに集約されています。ここの説明をざっと見てみると

Introduced in Android 3.0, loaders make it easy to asynchronously load data in an activity or fragment. Loaders have these characteristics:

・They are available to every Activity and Fragment.

・They provide asynchronous loading of data.

・They monitor the source of their data and deliver new results when the content changes.

・They automatically reconnect to the last loader's cursor when being recreated after a configuration change. Thus, they don't need to re-query their data.

非同期処理で行うための機構のようです。Fragmentでも非同期処理が利用できるように、新たに設計し直されたクラスでしょうか

以前は、AsyncTaskを利用して非同期処理を実現していたと思います。AsyncTaskの説明には次のように記述されています

AsyncTask enables proper and easy use of the UI thread. This class allows to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers

AsyncTaskは、HandlerやThreadを利用することなく、UI Threadを利用することでBackgroundの処理を行っていたようです。Loaderクラスの説明を見ると、UI Threadと言った単語は見受けられないので(実際やってみましたが、UI処理をすると例外を吐いて落ちます)、実行できることは同じですが、根本的に設計が異なるクラスのようです

AsyncTaskの動作イメージ

同じ画面のUIを制御するコードのはずですが、複数のクラスにまたがって埋め込まれてしまっています

AsyncTaskLoaderの動作イメージ

同じ画面で表示するProgressや非同期処理完了後のViewの更新処理など、UIの制御に関するコードは一箇所に固めることができました。AsyncTaskLoaderからは結果のみLoaderCallbackを通じて通知されています

次にAsyncTaskLoaderを継承して実装した場合、具体的にどのような違いがあるかを見ていきます

実装例(基本の基)

URLを指定し、Web上にある画像を取得し、それをImageViewに表示するという処理を考えてみます

AsyncTaskを用いる場合

public class DownloadImageAsyncTaskHelper extends AsyncTask<String, String, Bitmap> {

	@Override
	protected Bitmap doInBackground(String... urls) {
         String url = urls[0];
         
         // Progress Dialogを表示する
         publishProgress("Loading from " + url);
         return HttpUtil.getBitmapHttpService(context, url);
     }

	 @Override
     protected void onProgressUpdate(String... progress) {
         // ProgressDialogに文字列を設定する
         setProgressMessage(progress[0]);
     }

	 @Override
     protected void onPostExecute(Bitmap result) {
         // 非同期で読み込みが完了した画像をViewに表示する
         showImage(result);
     }
}

非同期処理に必要な初期パラメーター、途中経過を示すProgressのパラメーター、結果の型、など、なかなか指定するものが多いですね

さらに、doInBackground()で実行される非同期の画像読み込み処理とshowImage(), setProgerssMessage()等のUIの更新処理も含んでいます。これは、AsyncTaskはUI Thread上で実行されているため、UIの変更も出来てしまうのが原因です

AsyncTaskLoaderを用いる場合

同じようにURLを指定し、Web上の画像を取得し、ImageViewに表示する処理を実装してみます

public class DownloadImageAsyncTaskLoaderHelper extends AsyncTaskLoader<Bitmap> {

	private String imageUrl = "";
	private Context context = null;
	
	public DownloadImageAsyncTaskLoaderHelper(Context context, String url) {
		super(context);
		
		this.imageUrl = url;
		this.context = context;
	}

	@Override
	public Bitmap loadInBackground() {
		 return HttpUtil.getBitmapHttpService(context, url);
	}
	
	@Override
	protected void onStartLoading() {
		forceLoad();
	}
}

Progressの表示、非同期処理終了時のViewの更新などのUI制御のコードがなくなり、かなりすっきりしました。UI Threadが絡んでいないため、UIの変更はここでは行えません。もしAsyncTaskのようにUIの変更を行うとUI Thread以外からView変更を行った時にThrowされるいつもの例外が発生します

Caused by: android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

さらにAsyncTaskに比べて異なるのは、結果の型のみの指定になっています。では、どこで終了時のViewの更新などのUIの更新を行えば良いのでしょうか?

AsyncTaskLoaderの結果を受け取るには

LoaderCallbacksというInterfaceを実装してやることで、非同期処理の結果を受け取ることができます

一点注意すべきところがあります。SupportPakcageを利用する場合、Loaderを利用するためのメソッド(getSupportLoaderManager )が「FragmentActivity」に用意されているので、Activityで実行したい場合は「FragmentActivity」を継承してやる必要があります

public class MainActivity extends FragmentActivity implements LoaderCallbacks<Bitmap> {
	// snip
	
	// Loaderの初期化から起動までを行います
	public void startAsyncLoadImage(String url) {
		Bundle args = new Bundle();
		args.putString("url", url);
		getSupportLoaderManager().initLoader(0, args, this);	// onCreateLoaderが呼ばれます
		
		// 複数のLoaderを同時に動かす場合は、第一引数を一意のIDにしてやる必要があります。
		// GridViewなどに表示する画像を非同期で一気に取得する場合とか
	}
	
	@Override
	public Loader<Bitmap> onCreateLoader(int id, Bundle args) {
		
		// 非同期で処理を実行するLoaderを生成します.
		// ここを切り替えてあげるだけで様々な非同期処理に対応できます.
		if(args != null) {
			String url = args.getString("url");
			return new DownloadImageAsyncTaskLoaderHelper(this, url);
		}
	}
	
	@Override
	public void onLoadFinished(Loader<Bitmap> arg0, Bitmap arg1) {
	
		// 非同期処理が終了したら呼ばれます.
		// 今回はDownloadが完了した画像をImageViewに表示します.
		ImageView imageView = (ImageView)findViewById(R.id.imageview);
		Drawable iconImage = new BitmapDrawable(getResources(), bmp);
		imageView.setImageDrawable(iconImage);
		imageView.invalidate();
	}
	
	@Override
	public void onLoaderReset(Loader<Bitmap> arg0) {
		
	}
}

このように、非同期処理の部分とUI更新の部分を明確に分けることができます。UIの制御に関わる部分が一点に集中できコードが分散しないため、より役割分担が明確になりました。かなり見通しが良くなると思います

まとめ

  • AsyncTaskLoaderを使うと、UI制御と非同期処理の役割分担を明確に分けることができる
  • SupportPackageに入っているので、今すぐにでも使える
  • 何より最新の非同期処理実装なのでクールである(多分)

次回は、GridViewなどで一気に複数の画像を非同期で取得するための、AsyncTaskLoaderの実装例を見ながら、もう少し実用的な解説をしたいと思います