SwipeRefreshLayout の使い方と非同期処理
一般的に、時間のかかる操作を行う際に UI スレッドで直接操作してしまうと、ユーザーインターフェイスが固まった (ハングアップした) ような状態になってしまいます。 たとえ裏側で着実に何かを実行していたとしても、ユーザーから見ればアプリが反応しなくなるわけですから、「よく固まるアプリ」「不具合の多いソフト」ということになってしまいます。
そこで、外部からデータを取得するなど、時間のかかることが想定される操作を行う、あるいは、 ネットワークの接続などがあって終了時間がまちまちで読めない場合には、UI スレッド上では操作の開始だけを行い、 画面上には操作中を示すローディングサインを表示するのが望ましいものです。
画面のリフレッシュ操作でよく使われるのが、次のようなクルクル矢印アイコンです。
画面をスワイプしてリフレッシュ操作を開始し、終わり次第リフレッシュ結果を表示するものです。
SwipeRefreshLayout の使い方
SwipeRefreshLayout の基本
スワイプによるリフレッシュの開始、ローディングサインの表示と結果の表示を行うには SwipeRefreshLayout が利用できます。
SwipeRefreshLayout の子要素として ListView または GridView を配置します。
OnRefreshListener インターフェイスを実装したクラスを SwipeRefreshLayout のイベントリスナーに設定します。 すると、 スワイプ動作を行うとリフレッシュ操作が開始したとみなされクルクル矢印サインが表示され、 同時に onRefresh がコールバックされます。このタイミングで時間のかかる処理を開始します。
処理が終わったら、SwipeRefreshLayout の setRefreshing メソッドに false を渡して、 リフレッシュが終わったことを知らせます。これによってクルクル矢印サインが消えます。
少し実践的に。非同期処理と組み合わせる
上述のように、スワイプの認識やクルクル矢印の表示については、 SwipeRefreshLayout が自動的にやってくれるので非常に簡単です。
しかし実際のところ、SwipeRefreshLayout を効果的に使うには、「非同期処理」とか「非同期処理からの完了通知」といったところも分かっていないと、 上手く使えないので、もう少しこの点について補足しておきます。
非同期処理については「AsyncTask を利用した非同期処理」にも書いてます。
ここでは非同期処理の実行に関しては、AsyncTask を派生した非同期タスククラスを利用します。
AsyncTask は doInBackground メソッドをワーカースレッドで実行します。 これが返ったあと、UI スレッド上でその戻り値を引数にして onPostExecute メソッドを呼びます。
ですから、UI の更新に関わることについては、onPostExecute メソッドで行えば良いです。 UI を更新するには何らかの参照を AsyncTask (の派生) クラスに渡しておく必要があります。
WeakReference の利用
ここで注意が必要です。
もし非同期タスクオブジェクトに直接、 UI コンポーネントへの参照を渡すとどうなるでしょうか。
非同期タスクはあくまでも裏側で実行されていて、処理の完了は非常に時間がかかることが想定されています。 処理が完了する前に UI コンポーネントが不要になり破棄する必要がある状況になると、 本来は UI オブジェクトを破棄するべきところなのに非同期タスクが参照を握っているために破棄されません。
これによって、UI リソースが解放できなくなります。
そこで、WeakReference を経由して参照を持たせます。 これによって、非同期タスクがリソース解放を遮らないようにします。
コールバックインターフェイスを持たせる
WeakReference を使って参照を持たせるときには、クラス指定よりインターフェイス指定が使いやすいです。
例えば、参照を持たせるのに具体的な View クラスを指定していたとすると、持たせたい View クラスの種類が変わる度に非同期クラスを書き換える必要が発生してしまいます。 このためコールバックに使うオブジェクトの参照を持たせるには、「こんなメソッドを持ったオブジェクト」ということだけが指定できるインターフェイス指定が一般的です。
とまあ、くどくど書きましたが、一般的にイベントリスナーを指定するときにやっていることはそういうことです。
SwipeRefreshLayout と非同期タスクを使う具体例
説明は以上です。以下は具体例をみていきます。
「ListView の基本的な使い方」で説明した ListView のサンプルプログラムを書き換えて、 スワイプ動作によるリスト項目の追加を実装します。また、リフレッシュ動作は非同期タスクを利用して、時間のかかる処理の場合でも使えるようにします。
レイアウト activity_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">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ListView
android:id="@+id/listView1"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
<Button
android:text="Button"
android:id="@+id/button1"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
</LinearLayout>
メインアクティビティ MainActivity.java は次の通りです。
package com.keicode.test.refreshtest1;
import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity
implements
View.OnClickListener,
SwipeRefreshLayout.OnRefreshListener,
RefreshCallback {
static List<String> dataList = new ArrayList<>();
static ArrayAdapter<String> adapter;
ListView listView;
Button addButton;
SwipeRefreshLayout swipeRefresh;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViews();
setListeners();
setAdapters();
}
protected void findViews() {
listView = findViewById(R.id.listView1);
addButton = findViewById(R.id.button1);
swipeRefresh = findViewById(R.id.swipeRefresh);
}
protected void setListeners() {
addButton.setOnClickListener(this);
swipeRefresh.setOnRefreshListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button1:
addListItem("Button clicked!");
break;
}
}
protected void setAdapters() {
adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_list_item_1,
dataList);
listView.setAdapter(adapter);
}
@Override
public void onRefresh() {
new AddItemAsyncTask(this).execute();
}
@Override
public void addListItem(String s) {
adapter.add(s);
}
@Override
public void refreshCompleted() {
swipeRefresh.setRefreshing(false);
}
}
ここで、アクティビティが実装している RefreshCallback インターフェイスは次の通りです。
package com.keicode.test.refreshtest1;
interface RefreshCallback {
void addListItem(String s);
void refreshCompleted();
}
リフレッシュ操作は onRefresh メソッドで開始しています。
非同期タスククラスは次の通りです。
package com.keicode.test.refreshtest1;
import android.os.AsyncTask;
import android.os.SystemClock;
import java.lang.ref.WeakReference;
public class AddItemAsyncTask extends AsyncTask<Void, Void, String> {
private WeakReference<RefreshCallback> refreshCallbackReference;
AddItemAsyncTask(RefreshCallback refreshCallback) {
this.refreshCallbackReference = new WeakReference<>(refreshCallback);
}
@Override
protected String doInBackground(Void... params) {
try {
Thread.sleep(3000);
return "Uptime: " + SystemClock.uptimeMillis();
} catch (InterruptedException e) {
// Exception
}
return "";
}
@Override
protected void onPostExecute(String result) {
RefreshCallback callback = this.refreshCallbackReference.get();
if (callback != null) {
callback.addListItem(result);
callback.refreshCompleted();
}
}
}
以上、ここでは SwipeRefreshLayout の基本的な使い方と、 非同期タスクと組み合わせる方法について説明しました。