プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

【Android】 SQLite と CursorLoader の使い方(3/3): CursorLoader の利用

Content Provider も定義できたので、CursorLoaderを使う準備ができました。なお、(1/3)の記事(2/3)の記事と異なり、この記事のプロジェクトのみ、minSdkVersion=11 として行いました(minSdkVersionが11以前では、Loaderを使うためには、Support PackageのFragmentActivityを使う必要があるんだけど、めんどくさそうだったので後回しにしたためです)。

 

CursorLoaderの利用

SQLiteOpenHelper や Content Provider は(2/3)の記事までと同じです。ContentProvider の準備ができていれば CursorLoader を使うのは簡単です。

Google APIドキュメントListViewのサンプルパワフルなCursorLoader などを参考にActivityを作りました。今回のActivityは次のようにしました。

package com.mori_soft.android.sqlitesample.cursorloader;

import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.app.ListActivity;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.CursorAdapter;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.Toast;

public class MainActivity extends ListActivity implements LoaderManager.LoaderCallbacks {

    private SimpleCursorAdapter mAdapter;
    private MyAsyncQueryHandler mQueryHandler;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mAdapter = new SimpleCursorAdapter(
                this,
                android.R.layout.two_line_list_item,
                null,
                new String[] {SampleTblContract.NAME, SampleTblContract.AGE},
                new int[] {android.R.id.text1, android.R.id.text2},
                CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);

        // Bind to our new adapter.
        setListAdapter(mAdapter);
        
        mQueryHandler = new MyAsyncQueryHandler(this.getContentResolver());
        
        getLoaderManager().initLoader(0, null, (LoaderCallbacks) this);
    }
    
    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        Log.d("MainActivitiy", "called onCreateLoader");
        return new CursorLoader(this, SampleTblContract.CONTENT_URI, null, null, null, null);
    }

    @Override
    public void onLoadFinished(Loader loader, Cursor data) {
        Log.d("MainActivitiy", "called onLoaderFinished");
        mAdapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(Loader loader) {
        Log.d("MainActivitiy", "called onLoaderReset");
        mAdapter.swapCursor(null);
    }
    
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);

        Log.d("MainActivitiy", "called onListItemClick, id:" + id);

        // アイテムにタッチすると、そのアイテムを削除する
        /*
            Uri uri = ContentUris.withAppendedId(SampleTblContract.CONTENT_URI, id);
            getContentResolver().delete(uri, null, null);
         */

        // アイテムにタッチすると、年齢が増える
        Uri uri = ContentUris.withAppendedId(SampleTblContract.CONTENT_URI, id);
        //Uri uri = ContentUris.withAppendedId(Uri.parse("content://" + SampleTblContract.AUTHORITY + "/" + "invalid"), id);
        ContentValues cv = new ContentValues();
        cv.put(SampleTblContract.AGE, "99");
        
        // 非同期実行
        //getContentResolver().update(uri, cv, null, null);
        mQueryHandler.startUpdate(0, null, uri, cv, null, null);
    }

    private class MyAsyncQueryHandler extends AsyncQueryHandler {

        public MyAsyncQueryHandler(ContentResolver cr) {
            super(cr);
        }
        
        @Override
        public void startQuery(int token, Object cookie, Uri uri,
                String[] projection, String selection, String[] selectionArgs,
                String orderBy) {
            super.startQuery(token, cookie, uri, projection, selection, selectionArgs,
                    orderBy);
            // 呼び出し禁止
            throw new RuntimeException("invalid called for startQuery");
        }

        @Override
        protected void onInsertComplete(int token, Object cookie, Uri uri) {
            super.onInsertComplete(token, cookie, uri);
            Log.d("MyAsyncQueryHandler", "called onInsertComplete, uri:" + uri.toString());
        }

        @Override
        protected void onUpdateComplete(int token, Object cookie, int result) {
            super.onUpdateComplete(token, cookie, result);
            Log.d("MyAsyncQueryHandler", "called onUpdateComplete, result:" + result);
        }

        @Override
        protected void onDeleteComplete(int token, Object cookie, int result) {
            super.onDeleteComplete(token, cookie, result);
            Log.d("MyAsyncQueryHandler", "called onDeleteComplete, result:" + result);
        }

        @Override
        protected Handler createHandler(Looper looper) {
            //return super.createHandler(looper);
            return new ErrorHandler(looper);
        }
        
        private class ErrorHandler extends AsyncQueryHandler.WorkerHandler {

            public ErrorHandler(Looper looper) {
                super(looper);
            }
            
            @Override
            public void handleMessage(Message msg) {
                try {
                    super.handleMessage(msg);
                } catch (Exception e) {
                    Log.d("ErrorHandler", "exception occured:" + e.toString());
                    
                    // TODO
                    // 本当ならば、WorkerHandlerは別スレッドだからToastでエラーになるはず
                    // でも、なぜか問題なく動作した。たまたまか?
                    Toast.makeText(MainActivity.this, "SQLite問い合わせでエラーが発生しました", Toast.LENGTH_LONG).show();
                }
            }
        }
        
    }
}

CursorLoader のサンプルを探すと、Cursor を取得して ListView 等へ設定するというのが多いですが、query 以外の insert/update/delete を行う場合はどうすればよいか?というのがありません。でも、DBへのアクセスということを考えれば、queryだけでなく、これらのメソッドも別スレッドで処理するのが妥当ではないかと思います(ContentProvider insert() always runs on UI thread? などを参照)。

参照先にも記述がありますが、AndroidのAPIには都合の良いことに、AsyncQueryHandler というabstractクラスがあり、これを継承したクラスを使えば非同期で処理を行わせることが可能なようです。

上記のActivityでは、ContentResolver の query は CursorLoader 経由で処理されるので、insert/update/deleteメソッド呼出しについて、AsyncQueryHandler クラスを利用して処理するようにしました。

Android AsyncQueryHandler を使う

 

実験

実際に上記を動かしてみると、SQLite からデータを取得し、画面に表示されることがわかります(CursorLoaderが適切に別スレッドで処理してると信じてます)。

リスト中のアイテムにタッチすると、AsyncQueryHandler#updateメソッドが呼ばれます。AsyncQueryHandler のWorkerHandler#handeMessageにブレークポイントを設定しておくと、UIスレッド(mainスレッド)とは別のスレッドで処理が動いていることがわかります。

 

ちなみに、AsyncQueryHandler#onUpdateCompleteメソッドは下記に示すようにUIスレッドで呼び出されます。

 

エラーハンドリング

上記のAsyncTaskHandlerの実装は、『Android:AsyncQueryHandlerでクエリ発行時のエラーをハンドリングする方法』を参考にさせていただき、最初からエラーハンドリングを可能としています。

Activity#onListItemClick メソッド内の Uri を無効なものにして、実際にエラーを起こしてみます。

        //Uri uri = ContentUris.withAppendedId(SampleTblContract.CONTENT_URI, id);
        Uri uri = ContentUris.withAppendedId(Uri.parse("content://"
                            + SampleTblContract.AUTHORITY + "/" + "invalid"), id);
すると、下記に示すようにtry-catch節でエラーが取れることがわかります。

 

ところで、AsyntQueryHandler は UI スレッドではなく別スレッドで処理を行うのですが、Toastが問題なく表示されています。今までToastはUIスレッドじゃないと表示できないと思っていたのでこれは非常に不思議でした。どうも、調べてみるとToastが表示できないのは、UI スレッドという制限ではないようです。この辺のことは別記事にまとめておきます。

 

(参考情報)