AsyncTask#doInBackgroundの戻り値を考える

AsyncTaskって不親切よね

だってエラー処理がしにくいもの
doInBackgroundの戻り値がResultのみなので、非同期処理中にエラーが発生したとき
どんな理由でエラーが発生したとか、その時のメッセージはどれにするとか
指定することができません。

不親切なら自分でよくする

戻り値を独自のクラスにしてしまえばなんとでもなるよね

サンプルアプリつくる

画像をダウンロードしてくるアプリを作ろうと思います。
ありがちですね。ぐだぐだ設計します。
しばらく箇条書きが続くので、めんどい人は飛ば(ry

設計

処理の流れ
  • ネットワークから画像をダウンロードしてくる
  • 画像のダウンロードが終わるとUIに表示する
  • ネットワークの障害がある場合、その旨をユーザに伝える
クラスの設計とか
  • MainActivity
    • ユーザへの情報伝達は全てActivityが行う
  • DownloadImageTask
    • AsyncTaskでネットワークからデータをダウンロードしてくる
    • AsyncTask内でユーザへの情報伝達はProgressDialogのみ
  • DownloadImageTaskCallback
    • AsyncTaskからAcitivtyへの情報の伝達はコールバックを使う
  • AsyncTaskResult
    • AsyncTaskのdoInBackgroundメソッドからonPostExecuteメソッドへ渡す引数用の独自クラス
    • コンストラクタを使用できなくし、インスタンス作成メソッドを用意する
    • 他のAsyncTaskでも使えるよう、汎用的に作ります
MainActivityのUI設計
  • btnDownload:Button
    • ボタン押下で画像をダウンロードしてくる
    • ダウンロードした画像をimgViewerに表示する
  • imgViewer:ImageView
    • ダウンロードした画像を表示する場所
  • :Toast
    • メッセージを表示する
DownloadTaskのインターフェイス定義
  • onPreExecute():void
  • doInBackground(params:String[]):AsyncTaskResult
    • 画像のダウンロード
  • onProgressUpdate(values:Void[]):void
    • なにもしない
  • onPostExecute(:AsyncTaskResult):void
    • プログレスを閉じる
    • 結果をコールバックに返す
AsyncTaskResultのインターフェイス定義
  • getContent:T
    • タスクの実行結果を返す
  • getResourceId():int
    • エラーメッセージのリソースIDを返す
  • isError():boolean
    • タスクの実行結果がエラーの場合trueを返す
  • static createNormalResult(content:T):AsyncTaskResult
    • タスクの実行が成功した時の戻り値を作成する
  • static createErrorResult(resId:int):AsyncTaskResult
    • タスクの実行が失敗した時の戻り値を作成する
DownloadTaskCallbackのインターフェイス定義
  • onSuccessDownloadImage(image:Bitmap):void
    • ダウンロードが成功した時に呼ばれる
  • onFailedDownloadImage(statusNo:int):void
    • ダウンロードが失敗した時に呼ばれる

だいたい

こんな感じでもうコーディングしていいよね。
定義飽きたー!
だいたいこんなん誰が読むんだ!

ソース

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="jp.tomorrowkey.android.downloadimage"
  android:versionCode="1"
  android:versionName="1.0">
  <application
    android:icon="@drawable/icon"
    android:label="@string/app_name">
    <activity
      android:name=".MainActivity"
      android:label="@string/app_name">
      <intent-filter>
        <action
          android:name="android.intent.action.MAIN" />
        <category
          android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
  <uses-sdk
    android:minSdkVersion="4" />
  <uses-permission
    android:name="android.permission.INTERNET"></uses-permission>
</manifest>
string.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="app_name">DownloadImage</string>
  <string name="main.button.download">ダウンロード</string>
  <string name="dialog.message.downloading">ダウンロード中...</string>
  <string name="toast.not_found">画像がありません</string>
  <string name="toast.server_error">サーバーエラー</string>
  <string name="toast.uri_syntax_error">URLが不正です</string>
  <string name="toast.network_error">ネットワークエラー</string>
  <string name="toast.unkown_error">不明なエラー</string>
</resources>
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <Button
    android:id="@+id/btnDownload"
    android:text="@string/main.button.download"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />
  <ImageView
    android:id="@+id/imgViewer"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
</LinearLayout>
MainActivity.java
package jp.tomorrowkey.android.downloadimage;

import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

public class MainActivity extends Activity implements OnClickListener, DownloadImageTaskCallback {

  /* 
   * 画像のURL
   * 画像はpng形式でないとうまく動作しません(android1.6で確認)
   */
  private static final String IMAGE_URL = "http://xxx.xxx/image.png";

  /* ダウンロードボタン */
  private Button btnDownload;

  /* 画像を表示する部分 */
  private ImageView imgViewer;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    btnDownload = (Button) findViewById(R.id.btnDownload);
    btnDownload.setOnClickListener(this);
    imgViewer = (ImageView) findViewById(R.id.imgViewer);
  }

  @Override
  public void onClick(View view) {
    int id = view.getId();
    if (id == R.id.btnDownload) {
      // ダウンロードを開始する
      DownloadImageTask task = new DownloadImageTask(this, this);
      task.execute(IMAGE_URL);
    }
  }

  @Override
  public void onSuccessDownloadImage(Bitmap image) {
    // ダウンロードした画像を設定する
    imgViewer.setImageBitmap(image);
  }

  @Override
  public void onFailedDownloadImage(int resId) {
    // エラーの内容をトーストに表示する
    Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
  }
}
DownloadImageTaskCallback.java
package jp.tomorrowkey.android.downloadimage;

import android.graphics.Bitmap;

/**
 * DownloadImageTaskのコールバックインターフェイス
 * 
 * @author tomorrowkey
 * 
 */
public interface DownloadImageTaskCallback {
  /**
   * 画像のダウンロードが成功した時に呼ばれるメソッド
   * 
   * @param image
   *          ダウンロードした画像
   */
  void onSuccessDownloadImage(Bitmap image);

  /**
   * 画像のダウンロードが失敗した時に呼ばれるメソッド
   * 
   * @param resId
   *          エラーメッセージのリソースID
   */
  void onFailedDownloadImage(int resId);
}
DownloadImageTask.java
package jp.tomorrowkey.android.downloadimage;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import android.app.ProgressDialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;

/**
 * ネットワークから画像をダウンロードする非同期タスク
 * 
 * @author tomorrowkey
 * 
 */
public class DownloadImageTask extends AsyncTask<String, Void, AsyncTaskResult<Bitmap>> {

  private DownloadImageTaskCallback callback;
  private ProgressDialog progressDialog;

  public DownloadImageTask(Context context, DownloadImageTaskCallback callback) {
    this.callback = callback;

    // プログレスを作成する
    progressDialog = new ProgressDialog(context);
    progressDialog.setMessage(context.getText(R.string.dialog_message_downloading));
  }

  public void onPreExecute() {
    // プログレスを表示する
    progressDialog.show();
  }

  @Override
  public void onProgressUpdate(Void... values) {
  }

  @Override
  public AsyncTaskResult<Bitmap> doInBackground(String... params) {
    try {
      // 画像をダウンロードする
      HttpClient client = new DefaultHttpClient();
      HttpGet get = new HttpGet(new URI(params[0]));
      HttpResponse response = client.execute(get);
      int statusCode = response.getStatusLine().getStatusCode();
      switch (statusCode) {
      case HttpStatus.SC_OK:
        // 画像をダウンロードする
        InputStream is = response.getEntity().getContent();
        Bitmap bitmap = BitmapFactory.decodeStream(is);
        is.close();
        return AsyncTaskResult.createNormalResult(bitmap);
      case HttpStatus.SC_NOT_FOUND:
        // 画像がありません
        return AsyncTaskResult.createErrorResult(R.string.toast_not_found);
      default:
        // サーバーエラー
        return AsyncTaskResult.createErrorResult(R.string.toast_server_error);
      }
    } catch (URISyntaxException e) {
      // URLが不正です
      return AsyncTaskResult.createErrorResult(R.string.toast_uri_syntax_error);
    } catch (ClientProtocolException e) {
      // ネットワークエラー
      return AsyncTaskResult.createErrorResult(R.string.toast_network_error);
    } catch (IllegalStateException e) {
      // 不明なエラー
      return AsyncTaskResult.createErrorResult(R.string.toast_unkown_error);
    } catch (IOException e) {
      // 不明なエラー
      return AsyncTaskResult.createErrorResult(R.string.toast_unkown_error);
    }
  }

  public void onPostExecute(AsyncTaskResult<Bitmap> result) {
    // プログレスを閉じる
    progressDialog.dismiss();
    if (result.isError()) {
      // エラーをコールバックで返す
      callback.onFailedDownloadImage(result.getResourceId());
    } else {
      // ダウンロードした画像コールバックでを返す
      callback.onSuccessDownloadImage(result.getContent());
    }
  }
}
AsyncTaskResult.java
package jp.tomorrowkey.android.downloadimage;

/**
 * AsyncTaskのdoInBackgroundからonPostExecuteに渡す引数に仕様するクラス
 * 
 * @author tomorrowkey
 * 
 * @param <T>
 */
public class AsyncTaskResult<T> {
  /**
   * AsyncTaskで取得したデータ
   */
  private T content;
  /**
   * エラーメッセージのリソースID
   */
  private int resId;
  /**
   * エラーならtrueが設定されている
   */
  private boolean isError;

  /**
   * コンストラクタ
   * 
   * @param content
   *          AsyncTaskで取得したデータ
   * @param isError
   *          エラーならtrueを設定する
   * @param resId
   *          エラーメッセージのリソースIDを指定する
   */
  private AsyncTaskResult(T content, boolean isError, int resId) {
    this.content = content;
    this.isError = isError;
    this.resId = resId;
  }

  /**
   * AsyncTaskで取得したデータを返す
   * 
   * @return AsyncTaskで取得したデータ
   */
  public T getContent() {
    return content;
  }

  /**
   * エラーならtrueを返す
   * 
   * @return エラーならtrueを返す
   */
  public boolean isError() {
    return isError;
  }

  /**
   * stringリソースのIDを返す
   * 
   * @return stringリソースのIDを返す
   */
  public int getResourceId() {
    return resId;
  }

  /**
   * AsyncTaskが正常終了した場合の結果を作る
   * 
   * @param <T>
   * @param content
   *          AsyncTaskで取得したデータを指定する
   * @return AsyncTaskResult
   */
  public static <T> AsyncTaskResult<T> createNormalResult(T content) {
    return new AsyncTaskResult<T>(content, false, 0);
  }

  /**
   * AsyncTaskが異常終了した場合の結果を作る
   * 
   * @param <T>
   * @param resId
   *          エラーメッセージのリソースIDを指定する
   * @return AsyncTaskResult
   */
  public static <T> AsyncTaskResult<T> createErrorResult(int resId) {
    return new AsyncTaskResult<T>(null, true, resId);
  }
}

動作

ht-03a(android1.6)で動作を確認しています
IMAGE_URLにpng画像のURLを入れると画像がダウンロードされ、画面に表示されます。
jpgやgifだと画像のダウンロードはできますが、表示はできません。
ネットワークからのデコードに対応してないみたいです(android1.6)
urlに不正な値を入れてみるとエラーメッセージがトーストで表示されます。

まとめ

ここを読んでくれている人はいるのだろうか…。
コードが長すぎてもう読まねえよって言ってる人いるんじゃないだろうか…。
こんなにぺたぺたするならGitHubにあげろよって人いるんじゃないだろうか…。
くじょうはついったーでうけつけてます(´・ω・`)