로더는 Android 9 (API 수준 28)부터 지원 중단됩니다. Activity
및 Fragment
수명 주기를 처리하는 동안 데이터 로드를 처리하는 데 권장되는 옵션은 ViewModel
객체와 LiveData
의 조합을 사용하는 것입니다.
뷰 모델은 로더와 같이 구성을 변경할 때도 유지되지만 상용구 코드는 줄어듭니다. LiveData
는 여러 뷰 모델에서 재사용할 수 있는 수명 주기 인식 데이터 로드 방법을 제공합니다. MediatorLiveData
를 사용하여 LiveData
를 결합할 수도 있습니다.
Room 데이터베이스의 쿼리와 같이 관찰 가능한 모든 쿼리를 사용하여 데이터의 변경사항을 관찰할 수 있습니다.
ViewModel
및 LiveData
는 Service
와 같이 LoaderManager
에 액세스할 수 없는 상황에서도 사용할 수 있습니다. 이 두 가지를 함께 사용하면 UI 수명 주기를 처리하지 않고도 앱에서 필요한 데이터에 쉽게 액세스할 수 있습니다. LiveData
에 관한 자세한 내용은 LiveData
개요를 참고하세요. ViewModel
에 관한 자세한 내용은 ViewModel
개요를 참고하세요.
Loader API를 사용하면 콘텐츠 제공업체 또는 기타 데이터 소스에서 FragmentActivity
또는 Fragment
에 표시할 데이터를 로드할 수 있습니다.
로더가 없으면 다음과 같은 문제가 발생할 수 있습니다.
- 활동이나 프래그먼트에서 직접 데이터를 가져오면 UI 스레드에서 잠재적으로 느린 쿼리를 실행하여 사용자가 응답하지 않는 문제가 발생합니다.
AsyncTask
등을 사용해 다른 스레드에서 데이터를 가져오면 다양한 활동 또는 프래그먼트 수명 주기 이벤트(예:onDestroy()
, 구성 변경)를 통해 이 스레드와 UI 스레드를 모두 관리해야 합니다.
로더는 이러한 문제를 해결하며 다음과 같은 이점이 있습니다.
- 로더는 별도의 스레드에서 실행되어 UI가 느리거나 응답하지 않는 것을 방지합니다.
- 로더는 이벤트가 발생할 때 콜백 메서드를 제공하여 스레드 관리를 간소화합니다.
- 로더가 유지되고 구성 변경 시 결과를 캐시하여 중복 쿼리를 방지합니다.
- 로더는 관찰자를 구현하여 기본 데이터 소스의 변경사항을 모니터링할 수 있습니다. 예를 들어
CursorLoader
는 자동으로ContentObserver
를 등록하여 데이터가 변경될 때 새로고침을 트리거합니다.
Loader API 요약
앱에서 로더를 사용할 때 관련될 수 있는 여러 클래스와 인터페이스가 있습니다. 다음 표에 이러한 클래스가 요약되어 있습니다.
클래스/인터페이스 | 설명 |
---|---|
LoaderManager |
하나 이상의 Loader 인스턴스를 관리하기 위한 FragmentActivity 또는 Fragment 와 연결된 추상 클래스입니다. 활동 또는 프래그먼트당 하나의 LoaderManager 만 있지만 LoaderManager 는 여러 로더를 관리할 수 있습니다.
로더에서 데이터 로드를 시작하려면 |
LoaderManager.LoaderCallbacks |
이 인터페이스에는 로더 이벤트가 발생할 때 호출되는 콜백 메서드가 포함됩니다. 인터페이스는 세 가지 콜백 메서드를 정의합니다.
initLoader() 또는 restartLoader() 를 호출할 때 등록됩니다.
|
Loader |
로더가 데이터 로딩을 수행합니다. 이 클래스는 추상 클래스이며 모든 로더의 기본 클래스 역할을 합니다. Loader 를 직접 서브클래스로 분류하거나 다음과 같은 기본 제공 서브클래스 중 하나를 사용하여 구현을 간소화할 수 있습니다.
|
다음 섹션에서는 애플리케이션에서 이러한 클래스와 인터페이스를 사용하는 방법을 보여줍니다.
애플리케이션에서 로더 사용
이 섹션에서는 Android 애플리케이션 내에서 로더를 사용하는 방법을 설명합니다. 로더를 사용하는 애플리케이션에는 일반적으로 다음이 포함됩니다.
FragmentActivity
또는Fragment
LoaderManager
의 인스턴스입니다.ContentProvider
에서 지원하는 데이터를 로드하는CursorLoader
또는Loader
나AsyncTaskLoader
의 자체 서브클래스를 구현하여 다른 소스에서 데이터를 로드할 수도 있습니다.LoaderManager.LoaderCallbacks
의 구현입니다. 여기에서 새 로더를 만들고 기존 로더의 참조를 관리합니다.- 로더의 데이터를 표시하는 방법(예:
SimpleCursorAdapter
). CursorLoader
를 사용하는 경우ContentProvider
와 같은 데이터 소스.
로더 시작
LoaderManager
는 FragmentActivity
또는 Fragment
내에서 하나 이상의 Loader
인스턴스를 관리합니다. 활동 또는 프래그먼트당 LoaderManager
가 하나만 있습니다.
일반적으로 활동의 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()
호출은 로더가 초기화되고 활성 상태인지 확인합니다. 이로써 발생할 수 있는 결과가 두 가지 있습니다.
- 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: 제한을 사용하여 데이터가 변경될 때 콘텐츠 제공자가 실행하는 쿼리 수를 줄이는 방법의 예입니다.