로더

로더는 Android P(API 28)부터 사용이 중단되었습니다. 액티비티와 프래그먼트 수명 주기를 처리하면서 데이터 로딩을 처리할 때는 ViewModelsLiveData를 함께 사용하는 방법을 추천합니다. ViewModels는 로더와 같은 구성 변경에도 유지되지만 상용구가 적습니다. LiveData는 수명 주기를 인식하고 데이터를 로드하는 방식을 제공하는데, 이는 여러 ViewModels에서 다시 사용할 수 있습니다. MediatorLiveData를 사용해서 LiveData를 결합할 수도 있고, Room 데이터베이스에서 발생한 쿼리와 같이 모든 식별 가능한 쿼리를 사용하여 데이터에 대한 변경 사항을 식별할 수도 있습니다. ViewModels 및 LiveData는 LoaderManager에 액세스할 수 없는 상황(예: Service)에서도 사용할 수 있습니다. 두 가지를 동시에 사용하면 UI 수명 주기를 처리하지 않고도 앱이 필요로 하는 데이터에 간편하게 액세스할 수 있습니다. LiveData에 대한 자세한 내용은 LiveData 가이드를 참조하고 ViewModels에 대한 자세한 내용은 ViewModel 가이드를 참조하세요.

Loader API를 사용하면 콘텐츠 제공자 또는 FragmentActivity 또는 Fragment에서 표시하기 위한 다른 데이터 소스에서 데이터를 로드할 수 있습니다. Loader API에서 이렇게 사소해 보이는 작업을 처리해야 하는 이유를 알고 싶다면, 먼저 로더를 사용하지 않았을 때 발생할 수 있는 문제를 몇 가지 생각해보세요.

  • 액티비티나 프래그먼트에서 데이터를 직접 가져올 경우, UI 스레드에서 처리 속도가 느릴 가능성이 있는 쿼리를 실행하기 때문에 반응이 없을 수 있습니다.
  • 가령 AsyncTask를 사용해서 다른 스레드에서 데이터를 가져올 경우, 다양한 액티비티 또는 프래그먼트 수명 주기 이벤트(예: onDestroy()와 구성 변경사항)를 통해 스레드와 UI 스레드를 모두 관리해야 합니다.

로더는 이런 문제를 해결해줄 뿐만 아니라 다른 장점도 있습니다. 예를 들면 다음과 같습니다.

  • 로더는 별도의 스레드에서 실행되어 UI가 느리거나 반응이 차단되지 않도록 합니다.
  • 로더는 이벤트가 발생했을 때 콜백 메서드를 제공해 스레드 관리를 단순화합니다.
  • 로더는 구성이 변경되더라도 결과를 유지하고 캐싱하므로 중복 쿼리가 방지됩니다.
  • 로더는 관찰자를 구현해 기본 데이터 소스의 변경 사항을 모니터링할 수 있습니다. 예를 들어 CursorLoaderContentObserver를 자동으로 등록하여 데이터에 변경이 발생하면 새로고침을 트리거합니다.

Loader API 요약

앱에서 로더를 사용하는 데 관련된 클래스와 인터페이스는 여러 가지가 있습니다. 이 내용은 다음 표에 요약되어 있습니다.

클래스/인터페이스 설명
LoaderManager FragmentActivity 또는 Fragment와 연결된 추상 클래스로, 하나 이상의 Loader 인스턴스를 관리하는 데 쓰입니다. 각 액티비티나 프래그먼트에는 LoaderManager가 하나뿐이지만 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 애플리케이션 내에서 로더를 사용하는 방법을 설명합니다. 로더를 사용하는 애플리케이션에는 보통 다음이 포함되어 있습니다.

로더 시작

LoaderManagerFragmentActivity 또는 Fragment 내에서 하나 이상의 Loader 인스턴스를 관리합니다. 액티비티 또는 프래그먼트당 LoaderManager는 하나씩밖에 없습니다.

일반적으로 액티비티의 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.LoaderCallbacks 구현. 로더 이벤트를 보고하기 위해 LoaderManager가 호출합니다. 이 예시에서는 로컬 클래스가 LoaderManager.LoaderCallbacks 인터페이스를 구현하므로, 자신에 대한 참조인 this를 전달합니다.

initLoader() 호출로 로더가 초기화되고 활성 상태인지 확인합니다. 이로써 발생할 수 있는 결과가 두 가지 있습니다.

  • ID가 지정한 로더가 이미 존재하는 경우, 마지막으로 생성된 로더를 재사용합니다.
  • ID가 지정한 로더가 존재하지 않는 경우 initLoader()LoaderManager.LoaderCallbacks 메서드 onCreateLoader()를 트리거합니다. 여기에서 코드를 구현하여 새 로더를 인스턴스화하고 반환하는 것입니다. 자세한 내용은 onCreateLoader 섹션을 참조하세요.

어떤 경우든 주어진 LoaderManager.LoaderCallbacks 구현은 해당 로더와 관련되어 있으며, 로더 상태가 변경되면 이 구현이 호출됩니다. 이 호출의 시점에서 호출자가 시작된 상태에 있으며 요청한 로더가 이미 존재하고 자신의 데이터를 생성해 놓은 경우, 시스템은 initLoader() 중에 즉시 onLoadFinished()를 호출하므로 이런 일이 발생할 것에 대비해야 합니다. 이 콜백에 대한 자세한 내용은 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에는 다음과 같은 메서드가 포함됩니다.

  • 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 — 선택에 ?를 포함해도 됩니다. 이렇게 하면 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

이 메서드는 이전에 생성된 로더가 리셋 중이어서 해당 데이터를 사용할 수 없는 경우 호출됩니다. 이 콜백을 사용하면 데이터가 언제 해제될지 알아낼 수 있어 이에 대한 참조를 직접 제거할 수 있습니다.  

이 구현은 swapCursor()를 호출하고, 이때 값은 null입니다.

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를 사용하여 패키지 관리자에서 현재 설치된 앱을 로드하는 방법의 예시입니다.