ローダは Android P(API 28)で廃止されました。アクティビティとフラグメントのライフサイクルを処理している場合にデータの読み込みを行うには、ViewModels
と LiveData
を組み合わせて使用してください。ViewModels を使用すれば、ローダなどの設定の変更に対処できますが、ボイラープレートが削減されます。LiveData は、複数の ViewModels で再利用できるデータのライフサイクルに対応した読み込み方法を提供します。また、MediatorLiveData
を使用して LiveData を組み合わせれば、Room データベースからのクエリといった監視可能なクエリを使用してデータへの変更を監視できます。ViewModels と LiveData は、Service
内といった LoaderManager
にアクセスできない場合にも使用できます。これら 2 つを同時に使用すれば、UI のライフサイクルを気にせずに、アプリが必要なデータに簡単にアクセスできます。LiveData の詳細については LiveData ガイドをご覧ください。また、ViewModels については ViewModel ガイドをご覧ください。
ローダ API は、FragmentActivity
または Fragment
にデータを表示するために、コンテンツ プロバイダや他のデータソースからデータを読み込むのに使用します。このような些細に思える処理のためにローダ API が必要になる理由が理解できない方は、ローダがない場合に直面する可能性のある問題について考えてみてください。
- アクティビティまたはフラグメントのデータを直接取得すると、UI スレッドからのクエリの処理が遅いためにユーザーへの応答が遅れる。
AsyncTask
などを使用して別のスレッドからデータを取得すると、onDestroy()
や設定変更といったさまざまなアクティビティとフラグメントのライフサイクル イベント全体で、スレッドと UI スレッドの両方の管理をしなければならなくなる。
ローダはこのような問題に対処することができ、さらに多くの利点を提供します。たとえば、次のような利点があります。
- ローダは独立したスレッドで実行されるため、UI の品質が低下したり応答が遅くなったりするのを防止します。
- ローダはイベント発生時のコールバック メソッドを提供するため、スレッド管理が簡単になります。
- ローダは設定が変更されても結果を保持してキャッシュするため、クエリを繰り返し発行する必要がなくなります。
- ローダでは、基本的なデータソースの変更を監視するオブザーバを実装できます。たとえば、
CursorLoader
では自動的にContentObserver
が登録され、データの変更時に再読み込みが開始されます。
ローダ API の概要
アプリでローダを使用するときに必要になる可能性があるクラスやインターフェースは複数あります。それらの概要を以下の表に示します。
クラス/インターフェース | 説明 |
---|---|
LoaderManager |
1 つ以上の Loader インスタンスを管理するための、FragmentActivity や Fragment に関連した抽象クラスです。1 つのアクティビティやフラグメントごとに、LoaderManager は 1 つだけ存在しますが、LoaderManager は複数のローダを管理できます。
LoaderManager を取得するには、アクティビティまたはフラグメントから ローダでデータの読み込みを開始するには、 |
LoaderManager.LoaderCallbacks |
このインターフェースには、ローダイベントが発生した場合に呼び出されるコールバック メソッドが含まれます。次の 3 つのコールバック メソッドが定義されています。
initLoader() または restartLoader() を呼び出したときに登録されます。
|
Loader |
ローダはデータの読み込みを実行します。このクラスは抽象クラスであり、すべてのローダの基本クラスとなります。Loader のサブクラスを直接作成するか、以下の組み込みサブクラスのいずれかを利用して簡単に実装できます。
|
次のセクションでは、アプリでのこれらのクラスとインターフェースの使用方法を説明します。
アプリでローダを使用する
このセクションでは、Android アプリでのローダの使用方法について説明します。通常、ローダを使用するアプリには次の内容が含まれます。
FragmentActivity
またはFragment
。LoaderManager
のインスタンス。ContentProvider
でサポートされているデータを読み込むCursorLoader
。あるいは、Loader
やAsyncTaskLoader
の独自のサブクラスを実装して他のソースからデータを読み込むこともできます。LoaderManager.LoaderCallbacks
の実装。ここで新しいローダを作成して、既存のローダへの参照を管理します。SimpleCursorAdapter
などの、ローダのデータを表示する方法。CursorLoader
を使用するときの、ContentProvider
などのデータソース。
ローダを開始する
LoaderManager
は 1 つ以上の Loader
インスタンスを FragmentActivity
や Fragment
内で管理します。1 つのアクティビティやフラグメントごとに、LoaderManager
は 1 つだけ存在します。
通常は、アクティビティの onCreate()
メソッドか、フラグメントの onActivityCreated()
メソッド内で Loader
を初期化します。その方法は次のとおりです。
Kotlin
supportLoaderManager.initLoader(0, null, this)
Java
// Prepare the loader. Either re-connect with an existing one, // or start a new one. getSupportLoaderManager().initLoader(0, null, this);
initLoader()
メソッドが次のパラメータを受け取ります。
- ローダを識別する一意の ID。この例では、ID は 0 です。
- ローダの構築時に提供する任意の引数(この例では
null
)。 LoaderManager
がローダのイベントを報告する際に呼び出すLoaderManager.LoaderCallbacks
の実装。この例では、ローカルクラスがLoaderManager.LoaderCallbacks
インターフェースを実装するため、自身への参照を表すthis
を渡します。
initLoader()
の呼び出しによって、ローダが初期化され、アクティブになります。結果には次の 2 つの可能性があります。
- ID で指定されたローダが既に存在する場合は、最後に作成されたローダが再利用されます。
- ID で指定されたローダが存在しない場合、
initLoader()
がLoaderManager.LoaderCallbacks
メソッドonCreateLoader()
をトリガーします。ここで、新しいローダのインスタンスを作成して返すコードを実装します。詳細については、onCreateLoader のセクションをご覧ください。
いずれの場合でも、その LoaderManager.LoaderCallbacks
の実装はローダに関連付けられ、ローダの状態が変化したときに呼び出されます。この呼び出しの時点で、呼び出し側が開始された状態にあり、リクエストされたローダが既に存在し、データを生成済みの場合は、システムはただちに onLoadFinished()
を呼び出す(initLoader()
の間に)ため、それに備えておく必要があります。このコールバックの詳細については、onLoadFinished をご覧ください。
initLoader()
メソッドは作成された Loader
を返しますが、その参照をキャプチャする必要はないことに注意してください。LoaderManager
は自動的にローダの生存状態を管理します。LoaderManager
は必要に応じて読み込みの開始と停止を行い、ローダとそれに関連付けられたコンテンツの状態を管理します。このことからもわかるように、ローダと直接やり取りすることはほとんどありません(ローダの動作を微調整するためにローダ メソッドを使用する例については、LoaderThrottle のサンプルをご覧ください)。特定のイベントが発生したときに読み込み処理に割り込むことを目的に、LoaderManager.LoaderCallbacks
を使用することがよくあります。このトピックの詳細については、LoaderManager コールバックを使用するをご覧ください。
ローダを再起動する
上記のように initLoader()
を使用する場合、指定した ID を持つ既存のローダがあればそれを使用します。ない場合は新たに作成します。ただし、古いデータを破棄して最初からやり直したいこともあります。
古いデータを破棄するには、restartLoader()
を使用します。たとえば、この SearchView.OnQueryTextListener
の実装では、ユーザーのクエリが変化したときにローダを再起動します。新しい検索フィルタを使用して新しいクエリを実行できるように、ローダを再起動する必要があります。
Kotlin
fun onQueryTextChanged(newText: String?): Boolean { // Called when the action bar search text has changed. Update // the search filter, and restart the loader to do a new query // with this filter. curFilter = if (newText?.isNotEmpty() == true) newText else null supportLoaderManager.restartLoader(0, null, this) return true }
Java
public boolean onQueryTextChanged(String newText) { // Called when the action bar search text has changed. Update // the search filter, and restart the loader to do a new query // with this filter. curFilter = !TextUtils.isEmpty(newText) ? newText : null; getSupportLoaderManager().restartLoader(0, null, this); return true; }
LoaderManager コールバックを使用する
LoaderManager.LoaderCallbacks
はクライアントが LoaderManager
とやり取りできるようにするコールバック インターフェースです。
特に、CursorLoader
のローダでは、停止後もデータを保持しておくことが望まれます。これにより、アクティビティやフラグメントの onStop()
メソッドや onStart()
メソッドが呼び出されてもアプリでデータを維持することができるので、ユーザーがアプリに戻ったときにデータの再読み込みを待つ必要がありません。新しいローダを作成するタイミングを知りたいときや、ローダのデータの使用を停止するタイミングをアプリに伝えるときは、LoaderManager.LoaderCallbacks
メソッドを使用します。
LoaderManager.LoaderCallbacks
には次のメソッドが含まれています。
onCreateLoader()
— 指定された ID の新しいLoader
をインスタンス化して返します。
-
onLoadFinished()
— 前に作成されたローダが読み込みを完了したときに呼び出されます。
onLoaderReset()
— 前に作成されたローダがリセットされ、データが利用不可になったときに呼び出されます。
これらのメソッドについては、次のセクションで詳しく説明します。
onCreateLoader
ローダにアクセスしようとしたとき(たとえば、initLoader()
経由など)、ID で指定したローダが存在するかどうかを確認します。存在しない場合は、LoaderManager.LoaderCallbacks
メソッド onCreateLoader()
をトリガーします。ここで、新しいローダを作成します。通常は、CursorLoader
になりますが、独自の Loader
サブクラスを実装することもできます。
この例では、onCreateLoader()
コールバック メソッドが CursorLoader
を作成します。CursorLoader
はそのコンストラクタ メソッドを使用して構築する必要がありますが、ContentProvider
へのクエリを実行するのに必要なすべての情報が必要になります。具体的に必要な情報は次のとおりです。
- uri — 取得するコンテンツの URI。
- projection — 返す列のリスト。
null
を渡すとすべての列が返されるため、効率的ではありません。 - selection — SQL WHERE 句の書式で返す行を宣言するフィルタ(WHERE 自体は除く)。
null
を渡すと指定した URI のすべての行が返されます。 - selectionArgs — selection に ? を含めると、selection に出現する順序で selectionArgs の値に置き換えられます。この値は、Strings 配列でバインドされます。
- sortOrder — SQL ORDER BY 句(ORDER 自体は除く)の形式で行を順序付けします。
null
を渡すとデフォルトのソート順序が使用されるため、順序付けされない場合があります。
たとえば、次のようになります。
Kotlin
// If non-null, this is the current filter the user has provided. private var curFilter: String? = null ... override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> { // This is called when a new Loader needs to be created. This // sample only has one Loader, so we don't care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. val baseUri: Uri = if (curFilter != null) { Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, Uri.encode(curFilter)) } else { ContactsContract.Contacts.CONTENT_URI } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. val select: String = "((${Contacts.DISPLAY_NAME} NOTNULL) AND (" + "${Contacts.HAS_PHONE_NUMBER}=1) AND (" + "${Contacts.DISPLAY_NAME} != ''))" return (activity as? Context)?.let { context -> CursorLoader( context, baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, "${Contacts.DISPLAY_NAME} COLLATE LOCALIZED ASC" ) } ?: throw Exception("Activity cannot be null") }
Java
// If non-null, this is the current filter the user has provided. String curFilter; ... public Loader<Cursor> onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. This // sample only has one Loader, so we don't care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. Uri baseUri; if (curFilter != null) { baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(curFilter)); } else { baseUri = Contacts.CONTENT_URI; } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))"; return new CursorLoader(getActivity(), baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"); }
onLoadFinished
このメソッドは、前に作成されたローダが読み込みを完了したときに呼び出されます。このメソッドは、このローダに提供された最後のデータが解放される前に呼び出されることが保証されています。この時点で、すべての古いデータを削除する必要がありますが(まもなく解放されるため)、データの所有者はローダでありローダがデータを処理するため、自身でデータを解放しないようにしてください。
アプリがもうデータを使用していないことを検知すると、ローダがデータを解放します。たとえば、データが CursorLoader
からのカーソルの場合は、自身で close()
を呼び出さないようにしてください。カーソルが CursorAdapter
に置かれている場合は、古い Cursor
がクローズされないように swapCursor()
メソッドを使用する必要があります。たとえば、次のようになります。
Kotlin
private lateinit var adapter: SimpleCursorAdapter ... override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) adapter.swapCursor(data) }
Java
// This is the Adapter being used to display the list's data. SimpleCursorAdapter adapter; ... public void onLoadFinished(Loader<Cursor> loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) adapter.swapCursor(data); }
onLoaderReset
このメソッドは、前に作成されたローダがリセットされ、データが利用できなくなったときに呼び出されます。このコールバックにより、データが解放されるタイミングがわかり、そのデータへの参照を削除できます。
この実装では、null
の値を使用して swapCursor()
を呼び出します。
Kotlin
private lateinit var adapter: SimpleCursorAdapter ... override fun onLoaderReset(loader: Loader<Cursor>) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. adapter.swapCursor(null) }
Java
// This is the Adapter being used to display the list's data. SimpleCursorAdapter adapter; ... public void onLoaderReset(Loader<Cursor> loader) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. adapter.swapCursor(null); }
例
以下は、連絡先のコンテンツ プロバイダに対するクエリの結果が含まれた ListView
を表示する Fragment
の完全な実装の例です。CursorLoader
を使用してプロバイダへのクエリを管理しています。
この例にあるように、アプリがユーザーの連絡先にアクセスするには、マニフェストに READ_CONTACTS
のパーミッションを含める必要があります。
Kotlin
private val CONTACTS_SUMMARY_PROJECTION: Array<String> = arrayOf( Contacts._ID, Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS, Contacts.CONTACT_PRESENCE, Contacts.PHOTO_ID, Contacts.LOOKUP_KEY ) class CursorLoaderListFragment : ListFragment(), SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks<Cursor> { // This is the Adapter being used to display the list's data. private lateinit var mAdapter: SimpleCursorAdapter // If non-null, this is the current filter the user has provided. private var curFilter: String? = null override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) // Give some text to display if there is no data. In a real // application this would come from a resource. setEmptyText("No phone numbers") // We have a menu item to show in action bar. setHasOptionsMenu(true) // Create an empty adapter we will use to display the loaded data. mAdapter = SimpleCursorAdapter(activity, android.R.layout.simple_list_item_2, null, arrayOf(Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS), intArrayOf(android.R.id.text1, android.R.id.text2), 0 ) listAdapter = mAdapter // Prepare the loader. Either re-connect with an existing one, // or start a new one. loaderManager.initLoader(0, null, this) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { // Place an action bar item for searching. menu.add("Search").apply { setIcon(android.R.drawable.ic_menu_search) setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) actionView = SearchView(activity).apply { setOnQueryTextListener(this@CursorLoaderListFragment) } } } override fun onQueryTextChange(newText: String?): Boolean { // Called when the action bar search text has changed. Update // the search filter, and restart the loader to do a new query // with this filter. curFilter = if (newText?.isNotEmpty() == true) newText else null loaderManager.restartLoader(0, null, this) return true } override fun onQueryTextSubmit(query: String): Boolean { // Don't care about this. return true } override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) { // Insert desired behavior here. Log.i("FragmentComplexList", "Item clicked: $id") } override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> { // This is called when a new Loader needs to be created. This // sample only has one Loader, so we don't care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. val baseUri: Uri = if (curFilter != null) { Uri.withAppendedPath(Contacts.CONTENT_URI, Uri.encode(curFilter)) } else { Contacts.CONTENT_URI } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. val select: String = "((${Contacts.DISPLAY_NAME} NOTNULL) AND (" + "${Contacts.HAS_PHONE_NUMBER}=1) AND (" + "${Contacts.DISPLAY_NAME} != ''))" return (activity as? Context)?.let { context -> CursorLoader( context, baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, "${Contacts.DISPLAY_NAME} COLLATE LOCALIZED ASC" ) } ?: throw Exception("Activity cannot be null") } override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) mAdapter.swapCursor(data) } override fun onLoaderReset(loader: Loader<Cursor>) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. mAdapter.swapCursor(null) } }
Java
public static class CursorLoaderListFragment extends ListFragment implements OnQueryTextListener, LoaderManager.LoaderCallbacks<Cursor> { // This is the Adapter being used to display the list's data. SimpleCursorAdapter mAdapter; // If non-null, this is the current filter the user has provided. String curFilter; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // Give some text to display if there is no data. In a real // application this would come from a resource. setEmptyText("No phone numbers"); // We have a menu item to show in action bar. setHasOptionsMenu(true); // Create an empty adapter we will use to display the loaded data. mAdapter = new SimpleCursorAdapter(getActivity(), android.R.layout.simple_list_item_2, null, new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS }, new int[] { android.R.id.text1, android.R.id.text2 }, 0); setListAdapter(mAdapter); // Prepare the loader. Either re-connect with an existing one, // or start a new one. getLoaderManager().initLoader(0, null, this); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // Place an action bar item for searching. MenuItem item = menu.add("Search"); item.setIcon(android.R.drawable.ic_menu_search); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); SearchView sv = new SearchView(getActivity()); sv.setOnQueryTextListener(this); item.setActionView(sv); } public boolean onQueryTextChange(String newText) { // Called when the action bar search text has changed. Update // the search filter, and restart the loader to do a new query // with this filter. curFilter = !TextUtils.isEmpty(newText) ? newText : null; getLoaderManager().restartLoader(0, null, this); return true; } @Override public boolean onQueryTextSubmit(String query) { // Don't care about this. return true; } @Override public void onListItemClick(ListView l, View v, int position, long id) { // Insert desired behavior here. Log.i("FragmentComplexList", "Item clicked: " + id); } // These are the Contacts rows that we will retrieve. static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] { Contacts._ID, Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS, Contacts.CONTACT_PRESENCE, Contacts.PHOTO_ID, Contacts.LOOKUP_KEY, }; public Loader<Cursor> onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. This // sample only has one Loader, so we don't care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. Uri baseUri; if (curFilter != null) { baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(curFilter)); } else { baseUri = Contacts.CONTENT_URI; } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))"; return new CursorLoader(getActivity(), baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"); } public void onLoadFinished(Loader<Cursor> loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) mAdapter.swapCursor(data); } public void onLoaderReset(Loader<Cursor> loader) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. mAdapter.swapCursor(null); } }
その他の例
ローダの使用方法の例を以下に示します。
- LoaderCursor - 上記のスニペットの完全バージョン。
- 連絡先のリストを取得する -
CursorLoader
を使用して連絡先プロバイダからデータを取得する方法のチュートリアルです。 - LoaderThrottle - スロットリングを使用して、データの変更時にコンテンツ プロバイダが行うクエリ数を軽減する方法の例です。
AsyncTaskLoader
-AsyncTaskLoader
を使用してインストール済みのアプリを Package Manager から読み込む方法の例です。