正しいAsyncTaskLoaderの使い方

  • 10
    いいね
  • 0
    コメント

「AsyncTaskLoader 使い方」でググると誰一人正しい挙動をしていなかったので、AsyncTaskLoaderを利用したOneshotな非同期処理の実装方法について改めてまとめます。

AsyncTaskLoaderを実装する上で配慮すること

Activityが再起動しても、loadInBackgroundが再度呼ばれないこと

よくあるサンプルなどを見る限り、loadInBackground実行中にinitLoaderをすると再度forceLoadされます。
forceLoadを呼ぶと既に動いているloadInBackgroud実行スレッドはキャンセルされ、再度スレッドが作成されloadInBackgroundが実行されます。この挙動は間違っています。

公式サンプルではコンテンツの変更を考慮した実装になっているため、求めている挙動とは違った実装になっています。

AsyncTaskLoaderのabstractなサブクラスを用意する

AsyncTaskLoaderの面倒な挙動を吸収するために、abstractなサブクラスを作成します。
これでサブクラスはloadInBackgroundを実装するだけで良くなります。

mIsStartedフラグでforceLoadが複数回呼ばれることを防ぎます。

public abstract class AkkumaAsyncTaskLoader<D> extends AsyncTaskLoader<D> {

    private D mResult;
    private boolean mIsStarted = false;

    public AkkumaAsyncTaskLoader(Context context) {
        super(context);
    }

    @Override
    protected void onStartLoading() {
        if (mResult != null) {
            deliverResult(mResult);
            return;
        }
        if (!mIsStarted || takeContentChanged()) {
            forceLoad();
        }
    }

    @Override
    protected void onForceLoad() {
        super.onForceLoad();
        mIsStarted = true;
    }

    @Override
    public void deliverResult(D data) {
        mResult = data;
        super.deliverResult(data);
    }
}

Loader実装

面倒な処理はスーパークラスに書いたので、
行いたい非同期処理をloadInBackgroundに実装するだけで良いです。

public class SampleLoader extends AkkumaAsyncTaskLoader<String> {

    private String mExtraParam;

    public SampleLoader(Context context, String extraParam) {
        super(context);
        mExtraParam = extraParam;
    }

    @Override
    public String loadInBackground() {

        Log.d("SampleLoader", "loadInBackground");

        try {
            Thread.sleep(3000);
        } catch (InterruptedException ignored) {}

        return "task complete: " + mExtraParam;
    }
}

コールバックの受け取り

initLoader処理を始めたい時 または 既に走っている処理にLoaderCallbacksを再度設定したいときに呼びます。LoaderManagerではonDestroy時にLoaderCallbacksは破棄されるため、Activity再起動時のLoaderCallbacksを再度設定も必要です。

よってonCreateではデータを保持していない場合にinitLoaderするようにします。初期起動時 と 処理中にActivity再起動した場合のLoaderCallbacks再設定 両方をカバーできます。

Fragmentを利用している場合は、onActivityCreateで同様の実装を書きます。

public class MainActivity extends FragmentActivity {

    private static final int LOADER_ID = 1;
    private static final String SAVE_INSTANCE_TASK_RESULT = "info.akkuma.loader.MainActivity.SAVE_INSTANCE_TASK_RESULT";
    private static final String ARG_EXTRA_PARAM = "ARG_EXTRA_PARAM";

    private String mTaskResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState != null) {
            mTaskResult = savedInstanceState.getString(SAVE_INSTANCE_TASK_RESULT);
        }

        if (mTaskResult != null) {
            TextView textView = (TextView) findViewById(R.id.text_view);
            textView.setText(mTaskResult);
        }

        // Activityが結果を持っていない場合ロードを行う
        //
        // 結果を保持しない性質のリクエストの場合
        // getSupportLoaderManager().getLoader(LOADER_ID) != null という条件にもできる
        if (mTaskResult == null) {
            Bundle args = new Bundle();
            args.putString(ARG_EXTRA_PARAM, "サンプルパラメータ");
            getSupportLoaderManager().initLoader(LOADER_ID, args, mCallback);
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(SAVE_INSTANCE_TASK_RESULT, mTaskResult);
    }

    private final LoaderManager.LoaderCallbacks<String> mCallback = new LoaderManager.LoaderCallbacks<String>() {
        @Override
        public Loader<String> onCreateLoader(int id, Bundle args) {
            String extraParam = args.getString(ARG_EXTRA_PARAM);
            return new SampleLoader(MainActivity.this, extraParam);
        }

        @Override
        public void onLoadFinished(Loader<String> loader, String data) {
            getSupportLoaderManager().destroyLoader(loader.getId());

            // 結果は data に出てくる

            mTaskResult = data;

            TextView textView = (TextView) findViewById(R.id.text_view);
            textView.setText(mTaskResult);
        }

        @Override
        public void onLoaderReset(Loader<String> loader) {
            // do nothing
        }
    };
}