前回記事「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でカメラを起動し写真を撮って戻ってくると、きちんとリストが更新された。
コメント