ローダは Android 9(API レベル 28)で非推奨になりました。Activity
と Fragment
のライフサイクルの処理中にデータの読み込みを処理する場合は、ViewModel
オブジェクトと LiveData
を組み合わせて使用することをおすすめします。ビューモデルは、ローダーなどの構成変更後も存続しますが、ボイラープレート コードが少なくて済みます。LiveData
は、複数のビューモデルで再利用できるデータを読み込む、ライフサイクルを意識した方法を提供します。MediatorLiveData
を使用して LiveData
を組み合わせることもできます。Room データベースからのクエリなど、監視可能なクエリを使用して、データの変更を監視できます。
ViewModel
と LiveData
は、Service
など、LoaderManager
にアクセスできない状況でも使用できます。この 2 つを併用すると、UI ライフサイクルに対応しなくても、アプリが必要とするデータに簡単にアクセスできます。LiveData
の詳細については、LiveData
の概要をご覧ください。ViewModel
の詳細については、ViewModel
の概要をご覧ください。
Loader API を使用すると、コンテンツ プロバイダやその他のデータソースからデータを読み込んで、FragmentActivity
や Fragment
に表示させることができます。
ローダがない場合、次のような問題が発生する可能性があります。
- アクティビティまたはフラグメント内でデータを直接取得すると、UI スレッドからクエリが遅くなる可能性があるため、ユーザーが応答しにくくなるという問題が生じます。
- 別のスレッド(
AsyncTask
など)からデータを取得する場合は、onDestroy()
や構成変更など、さまざまなアクティビティまたはフラグメントのライフサイクル イベントを通じて、そのスレッドと UI スレッドの両方を管理することになります。
ローダはこれらの問題を解決し、他にも次のようなメリットがあります。
- UI の動作が遅くなったり反応しなくなったりするのを防ぐため、ローダは別のスレッドで実行されます。
- ローダは、イベントが発生したときにコールバック メソッドを提供することで、スレッドの管理を簡素化します。
- ローダは構成の変更後も結果を保持し、キャッシュに保存して、クエリの重複を防ぎます。
- ローダは、オブザーバーを実装して、基になるデータソースの変更をモニタリングすることができます。たとえば
CursorLoader
はContentObserver
を自動的に登録し、データが変更されたときに再読み込みをトリガーします。
Loader API の概要
アプリでローダーを使用する際に関係するクラスとインターフェースは複数あります。次の表にそれらの概要を示します。
クラス / インターフェース | 説明 |
---|---|
LoaderManager |
1 つ以上の Loader インスタンスを管理するために、FragmentActivity または Fragment に関連付けられた抽象クラス。LoaderManager は、アクティビティまたはフラグメントごとに 1 つだけですが、LoaderManager は複数のローダーを管理できます。
ローダーからのデータの読み込みを開始するには、 |
LoaderManager.LoaderCallbacks |
このインターフェースには、ローダー イベントが発生したときに呼び出されるコールバック メソッドが含まれています。このインターフェースは、次の 3 つのコールバック メソッドを定義します。
initLoader() または restartLoader() を呼び出すと登録されます。
|
Loader |
ローダはデータの読み込みを実行します。このクラスは抽象的で、すべてのローダの基本クラスとして機能します。Loader を直接サブクラス化するか、次の組み込みサブクラスのいずれかを使用することで、実装を簡素化できます。
|
次のセクションでは、アプリでこれらのクラスとインターフェースを使用する方法について説明します。
アプリケーションでローダを使用する
このセクションでは、Android アプリでローダを使用する方法について説明します。ローダを使用するアプリケーションには、通常、次のようなものがあります。
FragmentActivity
またはFragment
。LoaderManager
のインスタンス。ContentProvider
に基づくデータを読み込むCursorLoader
。また、Loader
またはAsyncTaskLoader
の独自のサブクラスを実装して、他のソースからデータを読み込むこともできます。LoaderManager.LoaderCallbacks
の実装。ここで、新しいローダを作成し、既存のローダへの参照を管理します。- ローダーのデータを表示する方法(
SimpleCursorAdapter
など)。 CursorLoader
を使用する場合のデータソース(ContentProvider
など)。
ローダーを開始する
LoaderManager
は、FragmentActivity
または Fragment
内で 1 つ以上の Loader
インスタンスを管理します。LoaderManager
は、アクティビティまたはフラグメントごとに 1 つだけ存在します。
通常は、アクティビティの onCreate()
メソッドまたはフラグメントの onCreate()
メソッド内で 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.LoaderCallbacks
実装。これはLoaderManager
が呼び出してローダ イベントを報告します。この例では、ローカルクラスがLoaderManager.LoaderCallbacks
インターフェースを実装しているため、それ自体(this
)への参照が渡されています。
initLoader()
を呼び出すと、ローダーが初期化され、アクティブになります。次の 2 つの結果が考えられます。
- ID で指定されたローダーがすでに存在する場合は、最後に作成されたローダが再利用されます。
- ID で指定されたローダーが存在しない場合、
initLoader()
はLoaderManager.LoaderCallbacks
メソッドonCreateLoader()
をトリガーします。ここで、新しいローダーをインスタンス化して返すコードを実装します。 詳しくは、onCreateLoader
のセクションをご覧ください。
いずれの場合も、特定の LoaderManager.LoaderCallbacks
実装はローダーに関連付けられ、ローダーの状態が変更されたときに呼び出されます。この呼び出しの時点で呼び出し元が開始状態にあり、リクエストされたローダがすでに存在し、そのデータを生成している場合、システムは initLoader()
中に直ちに onLoadFinished()
を呼び出します。そのためには、備えておく必要があります。このコールバックの詳細については、
onLoadFinished
のセクションをご覧ください。
initLoader()
メソッドは、作成された Loader
を返しますが、それへの参照をキャプチャする必要はありません。LoaderManager
は、ローダーのライフサイクルを自動的に管理します。LoaderManager
は、必要に応じて読み込みを開始および停止し、ローダーとその関連コンテンツの状態を保持します。
つまり、ローダを直接操作することはほとんどありません。通常は、特定のイベントが発生したときに読み込みプロセスに介入するために 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
を作成します。これには、ContentProvider
に対するクエリを実行するために必要な完全な情報セットが必要です。具体的には、以下のものが必要です。
- uri: 取得するコンテンツの URI。
- projection: 返される列のリスト。
null
を渡すとすべての列が返されるため、非効率的です。 - selection: 返す行を宣言するフィルタ。SQL の WHERE 句の形式で指定します(WHERE 自体は除きます)。
null
を渡すと、指定された URI のすべての行が返されます。 - selectionArgs: ?s を選択に含めると、selectionArgs の値によって、選択に出現する順序で置き換えられます。値は文字列としてバインドされます。
- sortOrder: SQL の ORDER BY 句(ORDER BY 自体を除く)の形式で行を並べ替える方法。
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
に置かれている場合は、swapCursor()
メソッドを使用して、古い Cursor
が閉じないようにします。次の例をご覧ください。
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 onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Prepare the loader. Either re-connect with an existing one, // or start a new one. loaderManager.initLoader(0, null, this) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, 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 } 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 onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Prepare the loader. Either re-connect with an existing one, // or start a new one. getLoaderManager().initLoader(0, null, this); } @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, 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); } @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: スロットリングを使用して、データが変更されたときにコンテンツ プロバイダが実行するクエリの数を減らす方法の例。