Android
AsyncTask

AsyncTaskだってすてたもんじゃないという話

More than 3 years have passed since last update.

最近、RxAndroidなどの話と絡めてなにかとdisられるAsyncTaskですが、そんなAsyncTaskもやればできる子ということを紹介したいと思います。

追記:
ActivityがOSによって破棄された場合に、クラッシュするケースについて、ご指摘いただいたので、shouldReject()メソッドを修正しました。
追記2:
shouldReject()で使っていたisDestroyed()メソッドがAPIレベル17以上限定だったので、hasWindowFocus()メソッドに差し替えました。

なにが悪いの?

disられるポイントは、おもに3つです。

  1. 例外処理が考慮されていないインターフェース。
  2. ActivityやFragmentのライフサイクルが考慮されてないので安全じゃない。
  3. 非同期処理の待ち合わせができない。

ここでは、AsyncTaskを継承したクラスを作ることで、上の2つを解決してみたいと思います。

メソッドを分ければいいじゃん

AsyncTaskには、エラーになったときのインターフェースはありませんが、キャンセルされたときのためのインターフェースは用意されています。そこで、こんな感じにして、エラーが起きたとき(例外が発生したとき)には、処理をキャンセルしてしまいます。
cancel()を呼んだとき、onPostExecute()が実行されないことが保証されているので、この段階で切り分けておいた方が安全です。

public class SafetyAsyncTask<Result> extends AsyncTask<Void, Void, Result> {

    private Exception exception;

    abstract protected Result doTask() throws Exception;
    abstract protected void onSuccess(Result result);
    abstract protected void onFailure(Exception exception);

    @Override
    protected Result doInBackground(Void... params) {
        try {
            return doTask();
        } catch (Exception e) {
            exception = e;
            cancel(true);
            return null;
        }
    }

    @Override
    protected void onCancelled() {
        if (exception != null) {
            onFailure(exception);
        }
        exception = null;
    }

    @Override
    protected void onPostExecute(Result result) {
        onSuccess(result);
    }
}

これを使うときは、こんな感じになります。

new SafetyAsyncTask<返す型>() {

  @Override
  protected 返す型 doTask() throws Exception {
      return 非同期処理を行って返値を返す。
  }

  @Override
  protected void onFailure(Exception exception) {
      エラー処理
  }

  @Override
  protected void onSuccess(返す型 result) {
      正常処理
  }

}.execute();

doTask()で行う処理では、例外を発生させてもcatchされてonFailure()が呼び出されるので、気兼ねなくI/O処理なんかも使えます。

一つ注意する点として、面倒なことをしたくなかったため、捕捉できる例外はフィールド変数で保持する1つだけなので、1つのSafetyAsyncTaskインスタンスが同時に複数回非同期処理を行うと、例外を取りこぼすことがあります。このあたりを厳密にやる場合は、キューを用意して複数の例外を保持できるようにしておいた方がよいでしょう。

ライフサイクルを考慮してみる

AsyncTaskはActivityやFragmentのライフサイクルを考慮していないので、onPostExecute()を実行するときには、すでに破棄されているかもしれません。そのようなとき、Viewの更新をしようとするとアプリはクラッシュします。

であれば、その状態を調べて、破棄されていたら処理を行わなければいいのです。

public class SafetyAsyncTask<Result> extends AsyncTask<Void, Void, Result> {

    private Exception exception;
    private Activity activity;
    private android.support.v4.app.Fragment fragment4;
    private android.app.Fragment fragment;

    abstract protected Result doTask() throws Exception;
    abstract protected void onSuccess(Result result);
    abstract protected void onFailure(Exception exception);

    public ApiTask(@NotNull Activity activity) {
        super();
        this.activity = activity;
    }

    public ApiTask(@NotNull android.support.v4.app.Fragment fragment) {
        super();
        this.fragment4 = fragment;
    }

    public ApiTask(@NotNull android.app.Fragment fragment) {
        super();
        this.fragment = fragment;
    }

    @Override
    protected Result doInBackground(Void... params) {
        try {
            return doTask();
        } catch (Exception e) {
            exception = e;
            cancel(true);
            return null;
        }
    }

    private boolean shouldReject() {

        Activity activity = this.activity;

        if (fragment4 != null) {
            activity = fragment4.getActivity();
        }
        if (fragmrnt != null) {
            activity = fragment.getActivity();
        }
        if (activity != null) {
            return activity.isFinishing() || !activity.hasWindowFocus();
        } else {
            return true;
        }
    }


    @Override
    protected void onCancelled() {
        if (! shouldReject()) {
            if (exception != null) {
                onFailure(exception);
            }
        }
        exception = null;
    }

    @Override
    protected void onPostExecute(Result result) {
        if (! shouldReject()) {
            onSuccess(result);
        }
    }
}

ベタですが、コンストラクタでActivityあるいはFragmentを渡すことによって保持し、処理を呼び出す前に、その状態を調べてActivityやFragmentが有効な場合だけ呼び出します。

hasWindowFocus()を判別に使っているので、破棄されたときというより隠れたときになってしまうので、若干、意味合いが変わってしまうのですが、あまり複雑にはしたくないので、これで良しとします。
APIレベル17(Android 4.2)以上限定ですが、activity.isDestroyed()を判別に使うこともできますので、場合に応じて使い分けることもできます。

ActivityやFragmentの状態に関係なく行わなければならない処理がある場合は、それを加えてもいいでしょう。

public class SafetyAsyncTask<Result> extends AsyncTask<Void, Void, Result> {

    :
    :

    abstract protected void onAlways(boolean reject);

    :
    :

    @Override
    protected void onCancelled() {

        : 
        :

        onAlways(shouldReject());
    }

    @Override
    protected void onPostExecute(Result result) {

        : 
        :

        onAlways(shouldReject());
    }
}

もっと楽をしたければ

この方法だと、シンプルに1つのクラスだけで実現しているため、見通しがよく、拡張がとても容易です。

「エラー処理なんてやること決まってるし」とか、「非同期処理中は、ProgressDialogだしておきたいよね」という場合は、こんな感じにしてしまえばよいでしょう。

public class SafetyAsyncTask<Result> extends AsyncTask<Void, Void, Result> {

    :
    :

    private Dialog dialog;

    :
    :

    private getContext() {
        if (activity != null) {
            return activity;
        } else if (fragment4 != null) {
            return fragment4.getActivity();
        } else if (fragment != null) {
            return fragment.getActivity();
        } else {
            //  ここにはこないはず
            return null;
        }
    }

    protected void onFailure(Exception exception) {
        Context context = getContext();
        if (context != null) {
            Toast.makeText(context, exception.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
        }    
    }

    @Override
    protected void onCancelled() {
        if (dialog != null) {
            dialog.dissmiss;
        }

        :
        :

    }

    @Override
    protected void onPostExecute(Result result) {
        if (dialog != null) {
            dialog.dissmiss;
        }

        :
        :

    }

    public SafetyAsyncTask with(Dialog dialog) {
        this.dialog = dialog;
        return this;
    }
}

ActivityやFragmentを保持しているためContextを得らことができるので、このクラス単独でも上記以外にもいろいろなエラー処理を実装できます。

まとめ

安全に非同期処理を行うのは、AsyncTaskでも可能です。

そのための面倒な記述は、AsyncTaskを継承したクラスで実装してしまえばいいのです。

PromissやReactといった新しい手法や概念は、それはそれで価値があると思うし、それらを使った方がよい場面も多々あると思いますが、AsyncTaskでも十分役に立つこともあるんじゃないでしょうか。

最後に

上記のコードは、実際に自分が使っているものを元に、この記事のために書き起こしたものです。

特に断りなく流用してもかまいませんが、あくまでも自己責任でお願いします。