CursorLoaderを使ったListView一覧表示

前回記事「SimpleCursorAdapterを用いたDBデータのリスト表示」で取り上げた通り、SimpleCursorAdapterに直接Cursorを渡す方法はUIスレッドでクエリー実行になってしまったり表示がDB更新と連動しないのでイマイチ。というわけで、そのあたりをすっきり解消してくれるという、CursorLoaderでの一覧表示を試してみる。

スポンサーリンク

CursorLoaderの正体

CursorLoaderを使おうと思った動機は上記の通り「クエリー実行のバックグラウンド化」と「リスト表示の自動更新」なのだが、実はCursorLoaderは簡単に言ってしまうとContentProviderとViewの間を媒介する代物ということが判明。というわけで「CursorLoaderを使う」イコール「ContentProviderを使う」という意味となる。

ContentProviderとは、他のアプリに自アプリが管理するDBをエクスポートする仕組み、と言えば良いだろうか?例えば、電話帳を色々なアプリから共有する際にも、ContentProviderの仕組みが使われている。うーん、予想と違ってなんだか大袈裟な感じだが、まぁともかく実装してみようか。

とはいえ、ContentProviderの仕組みを1から学ぶのは大変すぎる。そこで、似たような事をやろうとしているっぽい記事を見つけたので、参考にさせて頂いた。

ContentProvider派生クラスの定義

DBアダプタLogDBAdapterは、前記事で作成したものをそのまま流用。
今回はCursorを返すqueryメソッドとinsertメソッドだけあれば事足りるので、他の実装は省略。

queryメソッドで重要なのは、Cursorに対してsetNotificationUriを実行して、DB変更時に通知が行くようにする所。また、insertメソッドで重要なのはContentResolverに対してnotifyChangeメソッドを実行している所。これらをちゃんと実行しないと、DBへのデータ操作が正しくリストに反映されない。

public class MyContentProvider extends ContentProvider {
    private static final String AUTHORITY = "com.example.photologger.mycontentprovider";
    public static final Uri CONTENT_URI =
            Uri.parse("content://"+AUTHORITY+"/"+LogDBAdapter.PHOTO_TABLE_NAME);

    private static final String LOG_TAG = "PhotoLogger";

    static LogDBAdapter dba;

    public MyContentProvider() {
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
    return 0;
    }

    @Override
    public String getType(Uri uri) {
    Log.i(LOG_TAG, "MyContentProvider#getType()");
    return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        SQLiteDatabase db = dba.getWritableDatabase();

    long id = db.insert(LogDBAdapter.PHOTO_TABLE_NAME, null, values);

    Uri ret = ContentUris.withAppendedId(uri, id);
    getContext().getContentResolver().notifyChange(ret, null);
    Log.i(LOG_TAG, "MyContentProvider#insert(): Uri=" + ret.toString());

        return ret;
    }

    @Override
    public boolean onCreate() {
    dba = new LogDBAdapter(getContext());
    Log.i(LOG_TAG, "MyContentProvider#onCreate()");
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    Cursor c = dba.getURIcursor();
        c.setNotificationUri(getContext().getContentResolver(), uri);
    Log.i(LOG_TAG, "MyContentProvider#query()");
        return c;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,String[] selectionArgs) {
    return 0;
    }
}

MainActivityの変更

DBアダプタはContentProvider派生クラスの中に取り込んだので、MainActivityクラスの中では直接触らない。しかし内部的にCursorからデータ取得が行われるので、結局SimpleCursorAdapterを使うことになる。但し、前回直接Cursorをセットした時とは異なり、コンストラクタの引数ではCursorを指定する引数はnull。

MainActivityクラスからは直接Cursorは見えないものの、以下のように間接的にContentProviderにアクセスすることによってCursorを取得する、というかなり回りくどいことをする。

  • LoaderManager経由でContentProviderを初期化する
  • カメラintentから戻ってきたときは、getContentResolver()経由でContentProviderにアクセスして、ContentProviderのinsertメソッドを呼ぶ
public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {

    static final int REQUEST_IMAGE_CAPTURE = 1;
    SimpleCursorAdapter curAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

    String[] from = {"_id", LogDBAdapter.COL_PHOTO_URI};
    int[] to = {R.id.id, R.id.uri};
    curAdapter = new SimpleCursorAdapter(this, R.layout.list_item, null, from, to, FLAG_REGISTER_CONTENT_OBSERVER);

    ListView listView = (ListView)findViewById(R.id.listView);
    listView.setAdapter(curAdapter);

        getLoaderManager().initLoader(0, null, this); // Loader初期化

        FloatingActionButton fab = findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
        }
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        Uri PhotoUri = data.getData();

            ContentValues values=new ContentValues();
            values.put(LogDBAdapter.COL_PHOTO_URI,PhotoUri.toString());

        getContentResolver().insert(MyContentProvider.CONTENT_URI, values);
    } else {
        // キャンセルした時など
        Log.i("PhotoLogger","Failed to take photo.");
    }

    }

さて、MainActivityへのもう一つの大きな変更は、冒頭のクラス宣言の所。

public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>

これに伴って、@Override宣言で実装が必須となるメソッドが3つ存在する。

まず、onCreateLoaderメソッドではContentProviderと関連付けられたうえでCursorLoaderオブジェクトがnewされている。

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return new CursorLoader(
                this,
                MyContentProvider.CONTENT_URI,
                null,
                null,
                null,
                null
        );
    }

onLoadFinishedとonLoaderResetはSimpleCursorAdapterのメソッドswapCursorを呼んで、CursorとSimpleCursorAdapterを対応付ける役割を担っているのだろう。

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        curAdapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        curAdapter.swapCursor(null);
    }

ManifestへContentProviderの記述

上記だけでビルドは通るので実行してみると、リストに何も表示されない…ログを確認すると、以下のエラーを吐いている。

06-02 17:38:29.038 28164-29322/com.example.photologger E/ActivityThread: Failed to find provider info for com.example.photologger.mycontentprovider

これは上記コードでContentProviderのURIとしてMyContentProvider.CONTENT_URI(com.example.photologger.mycontentprovider)を指定しているのが「見つからない」と言っているエラーのようだ。

URIとContentProviderを関連付けるためには、以下のようなマニフェストへの記述が必要。

<provider android:name="MyContentProvider" android:authorities="com.example.photologger.mycontentprovider" />

AndroidManifest.xml全体は以下。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.photologger">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <provider android:name="MyContentProvider" android:authorities="com.example.photologger.mycontentprovider" />
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

前回とは異なり、今回はintentでカメラを起動し写真を撮って戻ってくると、きちんとリストが更新された。

コメント