ローダ

ローダは Android 9(API レベル 28)で非推奨になりました。ActivityFragment のライフサイクルの処理中にデータの読み込みを処理する場合は、ViewModel オブジェクトと LiveData を組み合わせて使用することをおすすめします。ビューモデルは、ローダーなどの構成変更後も存続しますが、ボイラープレート コードが少なくて済みます。LiveData は、複数のビューモデルで再利用できるデータを読み込む、ライフサイクルを意識した方法を提供します。MediatorLiveData を使用して LiveData を組み合わせることもできます。Room データベースからのクエリなど、監視可能なクエリを使用して、データの変更を監視できます。

ViewModelLiveData は、Service など、LoaderManager にアクセスできない状況でも使用できます。この 2 つを併用すると、UI ライフサイクルに対応しなくても、アプリが必要とするデータに簡単にアクセスできます。LiveData の詳細については、LiveData の概要をご覧ください。ViewModel の詳細については、ViewModel の概要をご覧ください。

Loader API を使用すると、コンテンツ プロバイダやその他のデータソースからデータを読み込んで、FragmentActivityFragment に表示させることができます。

ローダがない場合、次のような問題が発生する可能性があります。

  • アクティビティまたはフラグメント内でデータを直接取得すると、UI スレッドからクエリが遅くなる可能性があるため、ユーザーが応答しにくくなるという問題が生じます。
  • 別のスレッド(AsyncTask など)からデータを取得する場合は、onDestroy() や構成変更など、さまざまなアクティビティまたはフラグメントのライフサイクル イベントを通じて、そのスレッドと UI スレッドの両方を管理することになります。

ローダはこれらの問題を解決し、他にも次のようなメリットがあります。

  • UI の動作が遅くなったり反応しなくなったりするのを防ぐため、ローダは別のスレッドで実行されます。
  • ローダは、イベントが発生したときにコールバック メソッドを提供することで、スレッドの管理を簡素化します。
  • ローダは構成の変更後も結果を保持し、キャッシュに保存して、クエリの重複を防ぎます。
  • ローダは、オブザーバーを実装して、基になるデータソースの変更をモニタリングすることができます。たとえば CursorLoaderContentObserver を自動的に登録し、データが変更されたときに再読み込みをトリガーします。

Loader API の概要

アプリでローダーを使用する際に関係するクラスとインターフェースは複数あります。次の表にそれらの概要を示します。

クラス / インターフェース 説明
LoaderManager 1 つ以上の Loader インスタンスを管理するために、FragmentActivity または Fragment に関連付けられた抽象クラス。LoaderManager は、アクティビティまたはフラグメントごとに 1 つだけですが、LoaderManager は複数のローダーを管理できます。

LoaderManager を取得するには、アクティビティまたはフラグメントから getSupportLoaderManager() を呼び出します。

ローダーからのデータの読み込みを開始するには、initLoader() または restartLoader() を呼び出します。システムは、同じ整数 ID のローダがすでに存在するかどうかを自動的に判別し、新しいローダを作成するか、既存のローダを再利用します。

LoaderManager.LoaderCallbacks このインターフェースには、ローダー イベントが発生したときに呼び出されるコールバック メソッドが含まれています。このインターフェースは、次の 3 つのコールバック メソッドを定義します。
  • onCreateLoader(int, Bundle): システムが新しいローダを作成する必要がある場合に呼び出されます。コードで Loader オブジェクトを作成し、システムに返します。
  • onLoadFinished(Loader<D>, D): ローダーがデータの読み込みを完了したときに呼び出されます。通常は、コード内でユーザーにデータを表示します。
  • onLoaderReset(Loader<D>): 以前に作成したローダがリセットされたとき、destroyLoader(int) を呼び出したとき、またはアクティビティまたはフラグメントが破棄されたときに呼び出されます。これにより、データが利用できなくなっています。コード内で、ローダーのデータへの参照をすべて削除します。
アクティビティやフラグメントは通常このインターフェースを実装しており、initLoader() または restartLoader() を呼び出すと登録されます。
Loader ローダはデータの読み込みを実行します。このクラスは抽象的で、すべてのローダの基本クラスとして機能します。Loader を直接サブクラス化するか、次の組み込みサブクラスのいずれかを使用することで、実装を簡素化できます。

次のセクションでは、アプリでこれらのクラスとインターフェースを使用する方法について説明します。

アプリケーションでローダを使用する

このセクションでは、Android アプリでローダを使用する方法について説明します。ローダを使用するアプリケーションには、通常、次のようなものがあります。

ローダーを開始する

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 には次のメソッドがあります。

  • 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: スロットリングを使用して、データが変更されたときにコンテンツ プロバイダが実行するクエリの数を減らす方法の例。