【Android】AsyncTaskLoaderをもっと便利にする(実践編)

前回の続き…ってもう一ヶ月以上前なんですね。時の流れは早いものです。

別に書くのをサボっていたわけではなく(サボっていたけど)、まぁあの内容で大体なんとかなるので別に書かなくてもいっかなーと後回しにしていました。とりあえずもうクローズしてしまおうかなと。

コード(最終形)

GitHubにあげようとしたらなんでかコミットできないので全部書いてしまいます。

まずはAsyncTaskLoaderの結果をラップするやつ。

/**
 * ReactiveAsyncTaskで実行した結果を保持するクラス
 * エラーが発生した場合はerrorに格納される
 * @param <TReturn>
 */
public class ReactiveAsyncResult<TReturn> {
    private TReturn result;
    private Exception error;

    public TReturn getResult() { return this.result; }
    public void setResult(TReturn result) { this.result = result; }
    public Exception getError() { return this.error; }
    public void setError(Exception error) { this.error = error; }
    public boolean hasError() { return this.error != null; }

}

AsyncTaskLoaderのバグetcを取り除いた雛形。

import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;

public abstract class ReactiveAsyncLoader<TReturn> extends AsyncTaskLoader<ReactiveAsyncResult<TReturn>> {

    private ReactiveAsyncResult<TReturn> _data;

    public abstract boolean isComplete();

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

    @Override
    public void deliverResult(ReactiveAsyncResult<TReturn> data) {
        if (isReset()) {
            return;
        }
        _data = data;
        super.deliverResult(data);
    }

    @Override
    protected void onStartLoading() {
        if (_data != null) {
            deliverResult(_data);
        }

        if (takeContentChanged() || _data == null) {
            forceLoad();
        }
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }

    @Override
    protected void onReset() {
        super.onReset();
        onStopLoading();
        _data = null;
    }


}

LoaderCallbacksを実装したReactiveAsyncLoader専用のコールバック。

初回取得時の処理が欲しくなったので入れました。

import inujini_.function.Function.Action1;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;

public abstract class LoaderObserver<TReturn> implements LoaderCallbacks<ReactiveAsyncResult<TReturn>> {

    private ProgressDialog _prog;
    private Action1<TReturn> _onFirst;
    private boolean _isAllLoaded;

    // コンストラクタでProgressDialogを受け取った場合は初回時のみ表示させる
    public LoaderObserver() {}

    public LoaderObserver(ProgressDialog prog) {
        _prog = prog;
    }

    public LoaderObserver(Action1<TReturn> onFirst) {
        _onFirst = onFirst;
    }

    public LoaderObserver(ProgressDialog prog, Action1<TReturn> onFirst) {
        _prog = prog;
        _onFirst = onFirst;
    }

    @Override
    public Loader<ReactiveAsyncResult<TReturn>> onCreateLoader(int id, Bundle args) {
        if(_prog != null) _prog.show();
        return onCreate(id, args);
    }


    @Override
    public void onLoadFinished(Loader<ReactiveAsyncResult<TReturn>> loader, ReactiveAsyncResult<TReturn> data) {
        if(_prog != null) {
            _prog.dismiss();
            _prog = null;
        }

        if(_isAllLoaded) throw new IllegalStateException("これ以上データを読み込むことはできません。");

        if(!data.hasError()) {

            if(_onFirst == null) {
                onNext(data.getResult());
            } else {
                _onFirst.call(data.getResult());
                _onFirst = null;
            }

            if(((ReactiveAsyncLoader<TReturn>) loader).isComplete()) {
                onComplete();
                _isAllLoaded = true;
            }
        } else {
            onError(data.getError());
        }
    }

    @Override
    public void onLoaderReset(Loader<ReactiveAsyncResult<TReturn>> loader) {
        onReset((ReactiveAsyncLoader<TReturn>) loader);
    }

    public abstract ReactiveAsyncLoader<TReturn> onCreate(int id, Bundle args);
    public abstract void onNext(TReturn data);
    public abstract void onComplete();
    public abstract void onError(Exception e);
    public abstract void onReset(ReactiveAsyncLoader<TReturn> loader);


}

そうそう、諸般の事情からクロージャちゃんを自分で作りなおしました。理由は色々あるんですが、一番大きいのはあれがGPLだったことです。

二番目に大きな理由はC#ジェネリクスの指定方法が微妙に違っていたところです。と言うわけでついでに紹介しておきます。

public final class Function {
    private Function() {}

    public interface IAction {}

    public interface Action extends IAction {
        void call();
    }

    public interface Action1<T> extends IAction {
        void call(T p);
    }

    public interface Action2<T1, T2> extends IAction {
        void call(T1 p1, T2 p2);
    }

    public interface Action3<T1, T2, T3> extends IAction {
        void call(T1 p1, T2 p2, T3 p3);
    }

    public interface Action4<T1, T2, T3, T4> extends IAction {
        void call(T1 p1, T2 p2, T3 p3, T4 p4);
    }

    public interface Action5<T1, T2, T3, T4, T5> extends IAction {
        void call(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5);
    }

    public interface Actions extends IAction {
        void call(Object... args);
    }

    public interface IFunc {}

    public interface Func<R> extends IFunc {
        R call();
    }

    public interface Func1<T, R> extends IFunc {
        R call(T p);
    }

    public interface Func2<T1, T2, R> extends IFunc {
        R call(T1 p1, T2 p2);
    }

    public interface Func3<T1, T2, T3, R> extends IFunc {
        R call(T1 p1, T2 p2, T3 p3);
    }

    public interface Func4<T1, T2, T3, T4, R> extends IFunc {
        R call(T1 p1, T2 p2, T3 p3, T4 p4);
    }

    public interface Func5<T1, T2, T3, T4, T5, R> extends IFunc {
        R call(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5);
    }

    public interface Funcs<R> extends IFunc {
        R call(Object... args);
    }

    public interface Predicate<T> extends Func1<T, Boolean> {
        Boolean call(T p1);
    }
}

それぞれどんな仕組みで、なんでこんなものを作ったかは前回の記事を読んでください。

使ってみる

ReactiveAsyncLoaderを継承します。呼び出される度にカウントアップしますが、9を返した後は終わりです。そんな感じのもの。

public class TestLoader extends ReactiveAsyncLoader<String> {

    private boolean _isComplete;
    private Integer _count = 0;

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

    @Override
    public boolean isComplete() {
        return _isComplete;
    }

    @Override
    public ReactiveAsyncResult<String> loadInBackground() {

        ReactiveAsyncResult<String> r = new ReactiveAsyncResult<String>();
        r.setResult(_count.toString());

        _count++;

        if(_count < 10) {
            _isComplete = true;
        }

        return r;
    }

}

これを呼び出すActivityを作ります。とりあえずXMLはこんな感じ。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <Button
    android:id="@+id/btnCount"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="count up"
    android:onClick="countUp" />
</LinearLayout>

完全に作り忘れたとしか思えないんですが、LoaderManagerには「このidを持ったLoaderが作成されているかどうか」を確認するメソッドがありません。getLoaderメソッドの返り値がnullなら作成されてないです。クソですね。

そんな感じのActivity。

public class TestActivity extends FragmentActivity {

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


    public void countUp(View v) {
        LoaderManager manager = getSupportLoaderManager();
        Loader<String> loader = manager.getLoader(0);
        if(loader == null) {
            manager.initLoader(0, null, new LoaderObserver<String>() {
                @Override
                public ReactiveAsyncLoader<String> onCreate(int id, Bundle args) {
                    return new TestLoader(getApplicationContext());
                }

                @Override
                public void onNext(String data) {
                    Toast.makeText(getApplicationContext(), data, Toast.LENGTH_SHORT).show();
                }

                @Override
                public void onComplete() {
                    Toast.makeText(getApplicationContext(), "読み込み完了!", Toast.LENGTH_SHORT).show();
                }

                @Override
                public void onError(Exception e) {
                    e.printStackTrace();
                    Toast.makeText(getApplicationContext(), "エラー発生", Toast.LENGTH_SHORT).show();
                }

                @Override
                public void onReset(ReactiveAsyncLoader<String> loader) {
                    // Not implement
                }
            });
        } else {
            loader.forceLoad();
        }
    }
}

二度目以降はLoader#forceLoadを呼んであげる必要があります。再度初期化したら意味ないし。

後はボタンをボコボコクリックすると状況に応じたToastが表示されます。

応用してみる

今のままだと「読み込み完了!」が表示されてもボタンをクリックできます。そうなると例外が飛んできてしまいます。これはクールとは言えないですね。

何らかの形でこれ以上Loaderを呼ばせないようにしましょう。と言うわけで、onCompleteが呼び出されたらボタンを使用不可にします。

@Override
public void onComplete() {
    Toast.makeText(getApplicationContext(), "読み込み完了!", Toast.LENGTH_SHORT).show();
    ((Button) findViewById(R.id.btnCount)).setEnabled(false);
}

また、初回読み込みに限りProgressDialogを表示したり、特別なイベントを発生させることもできます。LoaderObserverのコンストラクタに渡すだけです。

if(loader == null) {
    ProgressDialog prog = new ProgressDialog(this);
    prog.setTitle("読み込み中");
    prog.setMessage("データを読み込んでいます...");
    prog.setProgressStyle(ProgressDialog.STYLE_SPINNER);

    manager.initLoader(0, null, new LoaderObserver<String>(prog, new Action1<String>() {
        @Override
        public void call(String data) {
            Toast.makeText(getApplicationContext(), "初回読み込みが完了しました。", Toast.LENGTH_SHORT).show();
        }
    }) {
        @Override
        public ReactiveAsyncLoader<String> onCreate(int id, Bundle args) {
            return new TestLoader(getApplicationContext());
        }
        
        // 省略

後は特にこれといった機能はないです。

まとめ

そんなわけでサクッと紹介しておきました。

こんな時間に書いたせいで全然やる気ないです。GitHubにあげられたら、追記しておきます。