Android Tips #32 CursorLoader で ContentProvider アクセスを非同期化する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ContentProvider アクセスには CursorLoader を使おう

Android アプリでデータベースを扱いたい場合は SQLite を使用しますが、他のアプリで使用している データベースへのアクセスには ContentProvider を使用して行います。
この ContentProvider に非同期でアクセスするための CursorLoader が Android 3.0 より導入されました (Support Package にも互換用クラスがあるので Android 1.6 から使用可能) 。今回はこの CursorLoader を使って簡単な電話帳一覧を作ってみたいと思います!今回も Support Package を使う場合の実装方法を解説します。

cursorloader01

CursorLoader の使いかた

CursorLoader は AsyncTaskLoader のサブクラスです。なので基本的に AsyncTaskLoader と使いかたは同じになります。AsyncTaskLoader については以前弊社小室が記事を書きましたので、そちらも参考にしてください。

1. LoaderManager で CursorLoader を読み込む

まずは FragmentActivity を継承した Activity をつくり FragmentActivity#getSupportLoaderManager で LoaderManager を読み込みます。LoaderManager は 複数の Loader を管理するためのクラスです。LoaderManager#initLoader() を呼びます。第三引数に渡す LoaderManager.LoaderCallbacks は次でつくります。下図は onCreate() で実装した場合です。せっかくの非同期なので ProgressDialog を表示するようにしています。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    // レイアウトを作成
    mListView = new ListView(this);
    setContentView(mListView);
    
    // ProgressDialog を表示
    mDialog = ProgressDialog.show(this, null, "Loading...");
    
    // CursorLoader を読み込む
    getSupportLoaderManager().initLoader(0, null, null);
}

2. LoaderManager.LoaderCallbacks を実装する

次に CursorLoader が実行されたあとの処理を書くために LoaderManager.LoaderCallbacks を実装します。 LoaderCallbacks を実装するときには Loader クラスで取り扱う 型を ジェネリクス型で指定できるので、 CursorLoader の場合は Cursor を指定します。
以下のメソッドをオーバーライドします。

onCreateLoader(int id, Bundle args) Loader が新しくインスタンス化されたときに呼ばれる。
onLoadFinished(Loader loader, D data) Loader の処理が終わったあとに呼ばれる。
onLoaderReset(Loader loader) 以前に作成した Loader がリセットされたときに呼ばれる。

単純に実装しただけだと以下のようになります。

private LoaderManager.LoaderCallbacks<Cursor> callbacks = new LoaderCallbacks<Cursor>() {
    
    @Override
    public void onLoaderReset(Loader<Cursor> arg0) {
    }
    
    @Override
    public void onLoadFinished(Loader<Cursor> arg0, Cursor arg1) {
    }
    
    @Override
    public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
        return null;
    }
};

3. CursorLoader をインスタンス化する

次に LoaderCallbacks#onCreateLoader() で CursorLoader をインスタンス化して返すようにします。CursorLoader のコンストラクタ引数には以下のような検索条件を渡すことができます。指定の必要なない場合は null を渡します。

第一引数 Context getApplidationContext() で OK
第二引数 Uri 対象とする ContentResolver の Uri
第三引数 String[] 取得するカラム
第四引数 String WHERE 句
第五引数 String[] WHERE 句の値
第六引数 String ORDER BY 句

今回は特に指定せずに取得してみました。

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
    Uri uri = People.CONTENT_URI;
    return new CursorLoader(getApplicationContext(), uri, null, null, null, null);
}

今回は Android 1.6 (APIレベル4) で動作する想定なので、電話帳アクセスの URI は People.CONTENT_URI を参照しています。Android 2.0 (APIレベル5) からは Contacts.CONTENT_URI で参照しましょう。

4. 非同期処理が終わったあとの処理を記述する

次に LoaderCallbacks#onLoadFinished() で CursorLoader の処理が終わったあとの処理を記述します。今回は CursorAdapter を使って ListView に表示してみました。

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    // ProgressDialog を閉じる
    mDialog.dismiss();
    // Adapter をつくり ListView にセットする
    PhoneCursorAdapter adapter = new PhoneCursorAdapter(getApplicationContext(), cursor, false);
    mListView.setAdapter(adapter);
}

PhoneCursorAdapter.java

private class PhoneCursorAdapter extends CursorAdapter {

    public PhoneCursorAdapter(Context context, Cursor c, boolean autoRequery) {
        super(context, c, autoRequery);
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        TextView nameView = (TextView) view.findViewById(android.R.id.text1);
        int nameColumn = cursor.getColumnIndexOrThrow(People.DISPLAY_NAME);
        String name = cursor.getString(nameColumn);
        nameView.setText(name);
        
        TextView numberView = (TextView) view.findViewById(android.R.id.text2);
        int numberColumn = cursor.getColumnIndexOrThrow(People.NUMBER);
        String number = cursor.getString(numberColumn);
        numberView.setText(number);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
        LayoutInflater inflater = getLayoutInflater();
        View view = inflater.inflate(android.R.layout.simple_list_item_2, viewGroup, false);
        return view;
    }

}

以上で完成です!

cursorloader01

まとめ

SQLite からデータを取得する処理スピードは端末の性能やデータ量に依存します。例えばユーザーがギャラリーにデータを大量に保存している場合、全件取得しようとするとかなり重たい処理になってしまいます。このような重い処理をメインスレッドで実装してしまうとアプリがフリーズしてしまう原因になるので、非同期で処理するべきです。
CursorLoader を使うと非同期処理がかなり簡単に実装できるので、ぜひ使っていきましょう!

参考