SwipeRefresh+AsyncTaskLoaderの使い方

SwipeRefreshLayout を使うと,引っ張って更新することができるようになります.様々なアプリで見られる基本的な機能で,これ自体は簡単に実装できます.

今回は,ローダと一緒に使うところで少し詰まってしまったので (というかローダの再利用のところをいまいち理解していなかったので),SwipeRefreshLayout で更新したときに AsyncTaskLoader を再利用し,CustomAdapter (ArrayAdapter) を使って ListView に反映するところまでをやりたいと思います.

SwipeRefreshLayout の実装

Gradle

build.gradle
dependencies {
    implementation 'com.android.support:support-v4:21.0.0'
}

予め build.gradle にサポートライブラリを読み込んでおきます.バージョンのところは適宜合わせてください.

以前は compile だったようですが,廃止されて現在は implementation となっています.また,バージョンのところで 21.0.+ と書くと (記憶の限りでは) 怒られるので,厳密に指定する必要があるっぽいです.

XML

list.xml
<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/swipe_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" />

</android.support.v4.widget.SwipeRefreshLayout>

SwipeRefreshLayout は内部に ListViewGridView を一つだけ持つことができます.

Java

Fragment.java
private SwipeRefreshLayout mSwipeRefresh;

...

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View rootView = inflater.inflate(R.layout.list, container, false);

    mSwipeLayout = rootView.findViewById(R.id.swipe_layout);
    // アニメーションの矢印の色を変更できる (デフォルトは黒)
    mSwipeLayout.setColorSchemeResources(R.color.red, R.color.yellow, R.color.green, R.color.blue)

    mSwipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            loadData(true);
        }
    })
}

TabLayout を使ったので Fragment で作っていますが,Activity でも大して変わらないと思います.

いくつかのサイトで紹介されていた setColorScheme() は既に廃止されていて,ID で指定する場合は setColorSchemeResources() を使います.使いたい色は [res] > [values] ディレクトリの color.xml に 16 進数の RGB で記述しておく必要があります.

更新時の動作は onRefresh() 内に記述します.

AsyncTaskLoader の処理

ローダ側で既に loadInBackground() は実装しているものとします.ネットワークに繋いでニュース記事を取得するなど,遅延が発生する処理はここに書いておきます.

メインスレッドでやろうとするとコンパイルエラーかなんかで弾かれます.

restartLoader()

Fragment.java
private void loadData(boolean b) {
    LoaderManager loaderManger = getLoaderManager();

    if (!b) {
        loaderManager.initLoader(LOADER_ID, null, this);
    } else {
        loaderManager.restartLoader(LOADER_ID, null, this);
    }
}

ローダを再利用するときに同一の ID を指定して restartLoader() を使うと,onCreateLoader() が呼ばれます.initLoader() は ID に対して一回しか使えません.二回目以降は onCreateLoader() が呼ばれないからです.

ArrayAdapter と ListView の処理

onCreateLoaderreturn new CustomLoader() し,処理が終わると onLoadFinish() が呼ばれるので,そこに ListView を更新する処理を書きます.

Fragment.java
private CustomAdapter mAdapter;

...

@Override public View onCreateView(...) {
    ...
    listView = rootView.findViewById(R.id.list_view)

    mAdapter = new CustomAdapter(getActivity(), new ArrayList<D>());
    listView.setAdapter(mAdapter);
}

...

@Override
public void onLoadFinished(Loader<List<D>> loader, List<D> items) {
    mAdapter.clear();

    if (items != null &amp;&amp; !items.isEmpty()) {
        mAdapter.addAll(items);
    }
    mAdapter.notifyDataSetChanged();

    mSwipeLayout.setRefreshing(false);
}

CustomAdapterArrayAdapter クラスを継承しています.

notifyDataSetChanged() がないと ListView が更新されないという話が合ったので書いてますが,実際どうなのかはわかりません.これはアダプタに対して clear()add() してないと呼ぶことができないようです.

setRefreshing(false) を呼ぶことで,アニメーションが終了します.これがないと,更新が終わっても回り続けます.更新ボタンを押したときに setRefreshing(true) になるような実装をすれば,引っ張らずとも更新できそうです.

参考