AsyncTaskLoaderとListFragmentを組み合わせたサンプル

Androidアプリ開発逆引きレシピに、リスト表示するサンプルアプリ「043 最後まで表示したら自動で項目が追加されるようにしたい」があります。このサンプルは、Activity、ListViewを使って実装しており、追加読み込みには、OnScrollListener と非同期処理を扱うAsyncTaskを使っています。

最近は、AsyncTaskではなく、AsyncTaskLoaderを使うのが流行りのようです。そこで、ListFragmentを組み合わせて、「text数字」という項目からなるリストを表示し、上にプルアップすると、さらにデータを読み込み表示するサンプルアプリを作ってみました。AsyncTaskLoaderでは、非同期処理を逐次的に行うのはそれほど自明ではないようで、ソースを下手に書くと、前に表示されていたリストデータと新たに追加されたリストデータが混在して表示されてしまいます。この点、AsyncTaskを使う場合、追加読み込みは、AsyncTaskのインスタンスを新たに生成し、executeメソッドを呼べばいいだけで、楽でした(※ 注: 非同期処理が走っているかのチェックをして、走っている場合は、追読み込みをしない処理は必要でした)。AsyncTaskLoaderを逐次的に呼べるようにした方法は以下の通りです。

* AsyncTaskLoaderを逐次的に行うようにするには、非同期処理を実行中かのmIsLoadingというboolean変数を導入し、非同期処理開始時、mIsLoadingをtrueにして、リストプルアップで呼ばれる additionalReadingメソッド内で、mIsLoadingの値をチェックをし、trueの場合、returnして、非同期処理を新たに呼ばないようにしました。1つの非同期処理が終了したとき呼ばれるonLoadFinished メソッド内で、mIsLoadingをfalseにしました。
* AsyncTaskLoaderを(逐次的に)複数回呼べるようにするために、コンストラクタに、非同期処理が何回目に呼ばれたかの引数を加え、AsyncTaskLoaderの初期化の際、initLoaderの第1引数に非同期処理が呼ばれる回数を入れました。

以下は実装したソースです。

ローダーのアイテム用データクラス

public class Entry {
    private String mLabel;

    public String getLabel() {
        return mLabel;
    }

    public void setLabel(String label) {
        mLabel = label;
    }

    @Override
    public String toString() {
        return mLabel;
    }
}

アダプター

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import java.util.List;


public class CustomAdapter extends ArrayAdapter<Entry> {
    private LayoutInflater mLayoutInflater;

    public CustomAdapter(Context context) {
        super(context, android.R.layout.simple_list_item_1);
        mLayoutInflater = (LayoutInflater)context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    public void setData(List<Entry> data) {
        clear();
        if (data != null) {
//            addAll(data); // addAll is usable from API level 11.
            for (Entry entry: data) {
                add(entry);
            }
        }
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // 特定の行(position)のデータを取得
        Entry item = (Entry)getItem(position);

        // 同じ行に表示されるViewは使い回しされるため初回だけ生成
        if (null == convertView) {
            convertView = mLayoutInflater.inflate(
                    R.layout.list_item, null);
        }

        // データをViewの各Widgetにセット
        TextView textView = (TextView)convertView.findViewById(R.id.text);
        textView.setText(item.toString());

        return convertView;
    }
}

AsyncTaskLoader

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

import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class CustomLoader extends AsyncTaskLoader<List<Entry>> {

    List<Entry> list;
    static List<Entry> sOldList;
    static List<Entry> sData;
    int mCount;
    int m;

    private static final String TAG = CustomLoader.class.getSimpleName();

    public CustomLoader(Context context, int count) {
        super(context);
        mCount = count;
    }

    /**
     * バックグラウンドでローダ用のデータを読み込む
     */
    @Override
    public List<Entry> loadInBackground() {

        // 2秒止める
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if (mCount == 0) {
            sData = new ArrayList<Entry>();
            m = 0;
        } else {
            // それまでのリスト
            sData = sOldList;
            m = sData.size();
        }
        Log.d(TAG, "m = " + m);

        for (int i = 0; i < 30; i++) {
            Entry entry = new Entry();
            entry.setLabel("text" + (i + m));
            sData.add(entry);
        }

       // Entry用のリストを作成
        List<Entry> entries = new ArrayList<Entry>(sData.size());

        for (int i = 0; i < sData.size(); i++) {
            Entry entry = sData.get(i);
            entries.add(entry);
        }

        // このソートは不要。AsyncTaskLoaderを何回も呼ぶ時、逐次的に、前の処理が終わったら
        // 次の処理を呼ぶ場合は、データ順序が正しくなるが、このアプリの実装初期、
        // 築時的に正しく呼ばれていなく、データの順序が正しくならなかったので、
        // リストのソートを実装した。
        // リストをソート
        Collections.sort(entries, CUSTOM_COMPARATOR);

        // entriesの内容をsOldListにコピー
        sOldList = new ArrayList<Entry>();
        for (Entry entry: entries) {
            sOldList.add(entry);
        }

        return entries;
    }

    /**
     * 提供する新しいデータがあるときに呼び出される
     */
    @Override
    public void deliverResult(List<Entry> data) {
        if (isReset()) {
            // リセット時(または最初に読み込みが開始されていない、もしくは
            // reset()が呼び出された後)現在の非同期クエリを解放
            if (data != null) {
                onReleaseResources(data);
            }
            return;
        }

        List<Entry> oldEntries = data;

        sData = data;

        if (isStarted()) {
            // 読み込みが開始されている (startLoading()が呼び出されているが
            // stopLoading()やreset()はまだ呼び出されていない)場合いn、その結果を返す
            super.deliverResult(data);
        }

        // この時点で、必要であれば oldEntries に関連するリソースを解放できる
        if (oldEntries != null && oldEntries != data) {
            onReleaseResources(oldEntries);
        }
    }

    @Override
    protected void onStartLoading() {
        forceLoad();
    }

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

    @Override
    public void reset() {
        super.reset();
        onStopLoading();
    }

    /**
     * 読み込んだデータ・セットに関連するリソースを解放するヘルパーメソッド
     */
    protected void onReleaseResources(List<Entry> apps) {
        // Cursorの場合は閉じる
        // 単純なリストList<>の場合は特に何もしない
    }

    /**
     * Entry用のComparator
     */
    public static final Comparator<Entry> CUSTOM_COMPARATOR =
            new Comparator<Entry>() {
//                private final Collator sCollator = Collator.getInstance();

                @Override
                public int compare(Entry object1, Entry object2) {
// 文字順だと、数字の順にならない
//                    return sCollator.compare(object1.getLabel(), object2.getLabel());
                    String str1 = object1.toString();
                    String str2 = object2.toString();
                    int length1 = str1.length();
                    int length2 = str2.length();
                    int num1 = Integer.valueOf(str1.substring(4, length1));
                    int num2 = Integer.valueOf(str2.substring(4, length2));
// 整数引数をとるCollator#compareはない
//                    return sCollator.compare(num1, num2);
                    if (num1 > num2) {
                        return +1;
                    } else if (num1 == num2) {
                        return 0;
                    } else {
                        return -1;
                    }
                }
            };
}

ListFragment

import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.util.Log;
import android.widget.AbsListView;

import java.util.List;

public class CustomListFragment extends ListFragment
        implements LoaderManager.LoaderCallbacks<List<Entry>>,
        AbsListView.OnScrollListener {

    private CustomAdapter mCustomAdapter;
    private boolean mIsLoading = false;
    private int mCount = 0;

    private final static String TAG = CustomListFragment.class.getSimpleName();

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        mCustomAdapter = new CustomAdapter(getActivity());
        setListAdapter(mCustomAdapter);

        // スクロールリスナーを設定
        getListView().setOnScrollListener(this);

        setListShown(false);

        mIsLoading = true;

        getLoaderManager().initLoader(0, null, this);
    }

    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        return new CustomLoader(getActivity(), mCount);
    }

    @Override
    public void onLoadFinished(Loader<List<Entry>> loader, List<Entry> data) {
        mCustomAdapter.setData(data);
        mIsLoading = false;
        setListShown(true);
        mCount++;
    }

    @Override
    public void onLoaderReset(Loader<List<Entry>> loader) {
        mCustomAdapter.setData(null);
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (totalItemCount == firstVisibleItem + visibleItemCount) {
            additionalReading();
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

    }

    private void additionalReading() {
        Log.d(TAG, "mIsLoading = " + mIsLoading);
        // 既に読み込み中ならスキップ
        if (mIsLoading) {
            return;
        }
        setListShown(false);
        mIsLoading = true;
        getLoaderManager().initLoader(mCount, null, this);
    }
}

MainActivity

import android.support.v4.app.FragmentActivity;
import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
import android.view.Menu;
import android.view.MenuItem;


public class MainActivity extends FragmentActivity {

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

        CustomListFragment frag = new CustomListFragment();
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.add(R.id.main, frag, "CustomListFragment");
        ft.commit();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

リストのレイアウトファイル list_item.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text=""/>

</LinearLayout>

Fragmentのレイアウトファイル fragment_sample.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
              android:orientation="vertical">

    <ListView
            android:id="@+id/ListView"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1" />

</LinearLayout>

Activityのレイアウトファイル activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              android:id="@+id/main"
              tools:context=".MainActivity">

</LinearLayout>

問題点
* データ読み込み開始時、setListShown(false)を呼ばないと、ずっとプログレスバーが表示され続ける。
* プログレスバーが画面中央に表示される。フッターの位置に表示したいのだけど、方法が不明。Activity + AsyncTaskでは簡単なんだけど。

GitHubに置いたソースコード
andropenguin/CustomListFragementSample4

参考
* Androidアプリ開発逆引きレシピ
* Android UI Cookbook for 4.0 ICS アプリ開発術

追記(Aug 20, 2014)
本記事中のコードには反映していないが、GitHubで、ソースコードに以下の変更を行う。上記問題点は解消された。
GitHubに置いたソースコード
andropenguin/CustomListFragementSample4
* プログレスバーの位置を画面下部に変える。
* レイアウトファイルで、ListViewのidを@id/android:listに変える。
* setListShownを使うとアプリが落ちるし、データ読み込み中、リストが非表示になるのは見栄えが悪いので、使用をやめる。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です