タケユー・ウェブ日報

Ruby on Rails や Flutter といったWeb・モバイルアプリ技術を武器にお客様のビジネス立ち上げを支援する、タケユー・ウェブ株式会社の技術ブログです。

SQLiteから読み込んだデータを画面に表示させつつ、データソースの変更に追従したい

## SQLiteの扱い ### SQLiteOpenHelper ### ContentProvider DB作成やマイグレーションなどは`SQLiteOpenHelper`を使うらしいことがわかった。 そのまま使うよりは、`ContentProvider`という仕組みを利用して、抽象化するのがナウいらしい。 [ContentProvider と SQLite](http://www.kojion.com/posts/view/12) > 総じて言うと行儀が良くなるという事だろう。 一般的な Android アプリでサーバ内の DB との通信を行う場合、 直接 DB とアクセスするのではなく、 WebAPI (REST) を実装してそれを介してアクセスするのと同じ事だ。 コンテンツプロバイダ - 愚鈍人 #### 外部に公開したくないデータを扱うとき コンテンツプロバイダにしたものの、あくまでも自分だけで使いたいときは ``` ``` のように`exported`属性を`false`にする。 ## コンテンツプロバイダにクエリを投げて結果をUIに表示 SQLiteで言えば、あるテーブルから一覧を取得し、ListViewに表示したいときなど ### SimpleCursorAdapter `SimpleCursorAdapter`にカーソルの扱いなど任せればとても簡単、変更があった際には自動でクエリを投げ直して画面の更新までやってくれるスグレモノ・・・という記事がネットに溢れてますが、これはAndroid 3.0で非推奨deprecatedということで使ってはいけないそう。 http://www.kojion.com/posts/view/15(http://www.kojion.com/posts/view/15) > SimpleCursorAdapter の flag を持たない方のコンストラクタは deprecated になっているが、 これは UI スレッドで自動で requery してしまうので決して使用しないこと。 これは CursorAdapter も同様であり、 autoRequery を行うかどうかのフラグをコンストラクタに渡せるので、それで必ず false を渡せばよい。 [Android:CursorAdapterコンストラクタの一部非推奨化 ](http://yuki312.blogspot.jp/2012/03/androidcursoradapter.html) > FLAG_AUTO_REQUERYは監視コンテンツの変更を検知した際に自動でrequeryさせるための > フラグです。 > しかし、requeryによるクエリ実行処理はUIスレッド上で行われるため、パフォーマンス > 上の観点から非推奨となりました。 > Android3.0でLoaderが追加されたように、UIスレッドでのクエリ実行は基本NGです。 SimpleCursorAdapterの方で変更監視・再クエリでまで任せてしまうと、UIスレッドで走ることになるのが問題ということでしょうか。 ### CursorLoader というわけで、`SimpleCursorAdapter`を使う場合、クエリを投げるのはUIスレッドの外になるよう、`CursorLoader`を使うことにする。 ### SimpleCursorAdapter + CursorLoader `CursorLoader`を使ってUIスレッドから独立してクエリを投げるものと、そうでないもの、初心者の僕にはよくわからなかったので試しに両方のサンプルを書いてみた。理解できた気がする。 どちらもSQLiteデータベースの内容を`ListView`に表示し、アイテムをタップするとカウントアップする、というもの。 #### SimpleCursorAdapterで楽してUI更新するサンプル(非推奨) https://github.com/uzuki05/android-CursorLoaderSample/blob/master/SQLiteCursorSample/src/main/java/com/takeyuweb/sqlitecursorsample/app/AutoRequeryActivity.java ``` package com.takeyuweb.sqlitecursorsample.app; import android.app.Activity; import android.content.ContentUris; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import gudon.sample.personprovider.Persons; /* * SimpleCursorAdapterにUI更新も任せてしまうサンプル ※3.0以降非推奨 * 変更の検知はコンテンツプロバイダで * c.setNotificationUri(getContext().getContentResolver(), uri); および * getContext().getContentResolver().notifyChange(uri, null); * を呼び出すことでUriベースで管理している(?) * * */ public class AutoRequeryActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_auto_requery); // (Uri uri, String projection, String selection, String selectionArgs, String sortOrder) final Cursor c = getContentResolver(). query(Persons.CONTENT_URI, null, null, null, null); SimpleCursorAdapter simpleCursorAdapter = new SimpleCursorAdapter( this, R.layout.list_item, c, new String {Persons.NAME, Persons.AGE}, new int {R.id.textViewName, R.id.textViewAge} ); ListView listView = (ListView)findViewById(R.id.listView); listView.setAdapter(simpleCursorAdapter); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { int age = c.getInt(c.getColumnIndex(Persons.AGE)); Uri uri = ContentUris.withAppendedId(Persons.CONTENT_URI, id); ContentValues values = new ContentValues(); values.put(Persons.AGE, age+1); getContentResolver().update(uri, values, null, null); } }); } } ``` #### CursorLoaderを使って非同期UI更新するサンプル https://github.com/uzuki05/android-CursorLoaderSample/blob/master/SQLiteCursorSample/src/main/java/com/takeyuweb/sqlitecursorsample/app/AsyncSwapCursorActivity.java ``` package com.takeyuweb.sqlitecursorsample.app; import android.app.Activity; import android.app.LoaderManager; import android.content.ContentUris; import android.content.ContentValues; import android.content.CursorLoader; import android.content.Loader; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.CursorAdapter; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import gudon.sample.personprovider.Persons; /* * CursorLoaderを用いてUIスレッドとは非同期でコンテンツプロバイダからデータを読み込み、反映するサンプル * * 参考 * http://www.mori-soft.com/2008-08-15-01-36-37/smartphone/109-android-sqlite-cursorloader-23-content-provider * http://ichitcltk.hustle.ne.jp/gudon/modules/pico_rd/index.php?content_id=75 * http://yuki312.blogspot.jp/2012/03/androidcursoradapter.html */ public class AsyncSwapCursorActivity extends Activity implements LoaderManager.LoaderCallbacks { private SimpleCursorAdapter simpleCursorAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_async_swap_cursor); simpleCursorAdapter = new SimpleCursorAdapter( this, R.layout.list_item, null, // 後々onLoadFinishedで設定するのでここではカーソルを指定しない new String {Persons.NAME, Persons.AGE}, new int {R.id.textViewName, R.id.textViewAge}, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER ); ListView listView = (ListView)findViewById(R.id.listView); listView.setAdapter(simpleCursorAdapter); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Uri uri = ContentUris.withAppendedId(Persons.CONTENT_URI, id); Cursor c = getContentResolver().query(uri, new String[] {Persons.AGE}, null, null, null); if (c.moveToFirst()) { int age = c.getInt(c.getColumnIndex(Persons.AGE)); ContentValues values = new ContentValues(); values.put(Persons.AGE, age + 1); getContentResolver().update(uri, values, null, null); } } }); LoaderManager loaderManager = getLoaderManager(); loaderManager.initLoader(0, null, this); } @Override public Loader onCreateLoader(int id, Bundle args) { return new CursorLoader(this, Persons.CONTENT_URI, null, null, null, null); } @Override public void onLoadFinished(Loader loader, Cursor c) { simpleCursorAdapter.swapCursor(c); } @Override public void onLoaderReset(Loader loader) { simpleCursorAdapter.swapCursor(null); } } ``` https://github.com/uzuki05/android-CursorLoaderSample(https://github.com/uzuki05/android-CursorLoaderSample)