로더는 Android 9 (API 수준 28)부터 지원 중단됩니다. Cloud Functions에 권장되는 옵션은
Activity
및 Fragment
수명 주기를 처리하면서 데이터 로드를 처리하는 방법은
ViewModel
객체의 조합
및 LiveData
입니다.
뷰 모델은 로더와 같은 구성 변경에도 유지되지만
상용구 코드가 적습니다. LiveData
는 다음에서 재사용할 수 있는 데이터를 로드하는 수명 주기 인식 방법을 제공합니다.
여러 개의 뷰 모델이 있습니다. 다음을 사용하여 LiveData
를 결합할 수도 있습니다.
MediatorLiveData
관찰 가능한 쿼리(예:
변경사항을 관찰하는 데 사용할 수 있는 Room 데이터베이스
데이터에 추가할 수 있습니다.
ViewModel
및 LiveData
은(는) 액세스 권한이 없는 경우에도 사용할 수 있습니다.
LoaderManager
에 전달(예:
Service
: 두 가지를
tandem은 UI를 처리하지 않고도 앱에서 필요한 데이터에 쉽게 액세스할 수 있는 방법을 제공합니다.
수명 주기 동안 사용됩니다 LiveData
에 관해 자세히 알아보려면 다음을 참고하세요.
LiveData
개요 자세히 알아보려면
ViewModel
에 관한 자세한 내용은 ViewModel
개요를 참고하세요.
Loader API를 사용하면
콘텐츠 제공업체
또는 FragmentActivity
에 표시할 다른 데이터 소스
또는 Fragment
.
로더가 없으면 다음과 같은 문제가 발생할 수 있습니다.
- 활동이나 프래그먼트에서 직접 데이터를 가져오면 사용자는 성능 저하 문제를 겪을 수 있음 쿼리를 실행하는 데 사용할 수 있습니다.
AsyncTask
를 사용하여 다른 스레드에서 데이터를 가져오면 해당 스레드와 스레드를 모두 관리하고 및 UI 스레드에 의해 실행될 수 있습니다(예:onDestroy()
및 구성 변경사항
로더는 이러한 문제를 해결하고 다음과 같은 다른 이점을 제공합니다.
- 로더는 별도의 스레드에서 실행되어 UI가 느리거나 응답하지 않도록 합니다.
- 로더는 이벤트 발생 시 콜백 메서드를 제공하여 스레드 관리를 단순화합니다. 발생할 수 있습니다
- 로더는 구성 변경 전반에 걸쳐 결과를 유지하고 캐시하여 중복 검색어입니다.
- 로더는 관찰자를 구현하여 기본
데이터 소스 예를 들어
CursorLoader
는 자동으로ContentObserver
를 등록하여 새로고침을 트리거합니다. 일어날 수 있습니다.
로더 API 요약
API를 사용할 때 관련된 클래스와 인터페이스는 여러 가지가 있습니다. 로더를 쓸 수 있습니다. 다음 표에 요약되어 있습니다.
클래스/인터페이스 | 설명 |
---|---|
LoaderManager |
FragmentActivity 또는
하나 이상을 관리하기 위한 Fragment
인스턴스 Loader 개 오직 하나
활동 또는 프래그먼트당 LoaderManager 이지만
LoaderManager 는 여러 로더를 관리할 수 있습니다.
로더에서 데이터 로드를 시작하려면 다음 중 하나를 호출합니다.
|
LoaderManager.LoaderCallbacks |
이 인터페이스에는
로더 이벤트가 발생합니다. 인터페이스는 다음과 같은 세 가지 콜백 메서드를 정의합니다.
<ph type="x-smartling-placeholder">
initLoader() 또는
restartLoader() 입니다.
|
Loader |
로더가 데이터 로딩을 수행합니다. 이 클래스는 추상 클래스이며
을 모든 로더의 기본 클래스로 사용합니다. kubectl run을 사용하여
Loader 또는 다음 기본 제공 중 하나 사용
서브클래스를 사용하여 구현을 간소화합니다.
<ph type="x-smartling-placeholder">
|
다음 섹션에서는 이러한 애플리케이션 내의 클래스와 인터페이스를 구축할 수 있습니다
애플리케이션에서 로더 사용
이 섹션에서는 Android 애플리케이션 내에서 로더를 사용하는 방법을 설명합니다. 로더를 사용하는 애플리케이션에는 일반적으로 다음이 포함됩니다.
FragmentActivity
또는Fragment
LoaderManager
의 인스턴스.ContentProvider
에서 지원하는 데이터를 로드하는CursorLoader
또는 자체 서브클래스를 구현할 수 있습니다. (Loader
또는AsyncTaskLoader
에서 다음 날짜까지) 다른 소스에서 데이터를 로드합니다.LoaderManager.LoaderCallbacks
의 구현입니다. 여기에서 새 로더를 만들고 기존 로더에 대한 참조를 관리합니다. 사용합니다- 로더의 데이터를 표시하는 방법(예:
SimpleCursorAdapter
) ContentProvider
등의 데이터 소스는CursorLoader
입니다.
로더 시작
LoaderManager
는 FragmentActivity
내에서 하나 이상의 Loader
인스턴스를 관리합니다.
Fragment
입니다. 활동 또는 프래그먼트당 하나의 LoaderManager
만 있습니다.
일반적으로
활동의 onCreate()
메서드 또는 프래그먼트의 메서드 내에서 Loader
초기화
onCreate()
메서드를 사용하여 지도 가장자리에
패딩을 추가할 수 있습니다. 나
다음 단계를 따르세요.
Kotlin
supportLoaderManager.initLoader(0, null, this)
자바
// 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
구현은 로더와 연결되고
로더 상태가 변경됩니다. 통화 시작 시 발신자가
요청된 로더가 이미 존재하며
데이터가 있는 경우 시스템은 onLoadFinished()
를 호출합니다.
initLoader()
중에 즉시 적용됩니다. 이에 대한 준비가 되어 있어야 합니다. 이 콜백에 대한 자세한 내용은
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 }
자바
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") }
자바
// 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) }
자바
// 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) }
자바
// 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) } }
자바
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); } }
예시 더보기
다음 예시는 로더의 사용 방법을 나타냅니다.
- <ph type="x-smartling-placeholder"></ph> LoaderCursor: 이전 스니펫의 전체 버전입니다.
- 연락처 목록을 가져옵니다.
CursorLoader
를 사용하여 검색하는 둘러보기 데이터를 가져올 수 있습니다. - <ph type="x-smartling-placeholder"></ph> LoaderThrottle: 제한을 사용하여 애플리케이션 개수를 줄이는 방법의 예 쿼리가 실행될 때 쿼리하기만 하면 됩니다.