Загрузчики не поддерживаются начиная с Android 9 (уровень API 28). Рекомендуемый способ загрузки данных при обработке жизненных циклов Activity и Fragment – использовать сочетание объектов ViewModel и LiveData.
Модели представлений, как и загрузчики, сохраняются при изменении конфигурации, но требуют меньше шаблонного кода. LiveData позволяет загружать данные с учетом жизненного цикла, и его можно использовать в нескольких моделях представления. Вы также можете объединить LiveData с помощью MediatorLiveData.
Любые наблюдаемые запросы, например из базы данных Room, можно использовать для отслеживания изменений в данных.
ViewModel и LiveData также доступны в ситуациях, когда у вас нет доступа к LoaderManager, например в Service. Используя их в тандеме, вы получаете простой способ доступа к данным, необходимым вашему приложению, без необходимости иметь дело с жизненным циклом пользовательского интерфейса. Подробнее о LiveData
LiveData… Подробнее ViewModel рассказывается в обзоре ViewModel.
Loader API позволяет загружать данные из поставщика контента или другого источника данных для отображения в FragmentActivity или Fragment.
Без загрузчиков могут возникнуть следующие проблемы:
- Если вы получаете данные непосредственно в действии или фрагменте, пользователи могут столкнуться с задержками из-за выполнения потенциально медленных запросов из потока UI.
- Если вы получаете данные из другого потока, например с помощью
AsyncTask, то вам нужно управлять как этим потоком, так и потоком UI через различные события жизненного цикла действия или фрагмента, такие какonDestroy()и изменения конфигурации.
Загрузчики решают эти проблемы и имеют другие преимущества:
- Загрузчики работают в отдельных потоках, чтобы предотвратить медленную работу или отсутствие ответа от пользовательского интерфейса.
- Загрузчики упрощают управление потоками, предоставляя методы обратного вызова при возникновении событий.
- Загрузчики сохраняют и кешируют результаты при изменении конфигурации, чтобы предотвратить дублирование запросов.
- Загрузчики могут использовать наблюдателя для отслеживания изменений в базовом источнике данных. Например,
CursorLoaderавтоматически регистрируетContentObserver, чтобы инициировать перезагрузку при изменении данных.
Обзор Loader API
При использовании загрузчиков в приложении могут быть задействованы несколько классов и интерфейсов. Они перечислены в таблице ниже.
| Класс/интерфейс | Описание |
|---|---|
LoaderManager |
Абстрактный класс, связанный с FragmentActivity или Fragment и предназначенный для управления одним или несколькими экземплярами Loader. Для каждого действия или фрагмента существует только один объект
LoaderManager, но он
LoaderManager может управлять несколькими загрузчиками.
Чтобы получить Чтобы начать загрузку данных, вызовите |
LoaderManager.LoaderCallbacks |
Этот интерфейс содержит методы обратного вызова, которые вызываются при возникновении событий загрузчика. Интерфейс определяет три метода обратного вызова:
initLoader() или
restartLoader().
|
Loader |
Загрузчики выполняют загрузку данных. Это абстрактный класс, который служит базовым для всех загрузчиков. Вы можете создать подкласс Loader напрямую или использовать один из следующих встроенных подклассов, чтобы упростить реализацию:
|
В следующих разделах рассказывается, как использовать эти классы и интерфейсы в приложении.
Как использовать загрузчики в приложении
В этом разделе описывается, как использовать загрузчики в приложении для Android. Приложение, использующее загрузчики, обычно включает в себя следующее:
FragmentActivityилиFragment.- Экземпляр
LoaderManager. CursorLoaderдля загрузки данных, поддерживаемыхContentProvider. Вы также можете реализовать собственный подклассLoaderилиAsyncTaskLoader, чтобы загружать данные из другого источника.- Реализация для
LoaderManager.LoaderCallbacks. Здесь вы можете создавать загрузчики и управлять ссылками на существующие. - Способ отображения данных загрузчика, например
SimpleCursorAdapter. - Источник данных, например
ContentProvider, при использованииCursorLoader.
Запустить загрузчик
LoaderManager управляет одним или несколькими экземплярами Loader в FragmentActivity или Fragment. Для каждого объекта activity или фрагмента существует только один объект LoaderManager.
Обычно инициализация Loader выполняется в методе onCreate() действия или методе onCreate() фрагмента. Для этого выполните следующие действия:
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() принимает следующие параметры:
- Уникальный идентификатор загрузчика. В этом примере идентификатор –
0. - Необязательные аргументы, которые передаются загрузчику при создании (в этом примере –
null). - Реализация
LoaderManager.LoaderCallbacks, которуюLoaderManagerвызывает для передачи событий загрузчика. В этом примере локальный класс реализует интерфейсLoaderManager.LoaderCallbacks, поэтому он передает ссылку на себя –this.
Вызов initLoader() гарантирует, что загрузчик инициализирован и активен. Возможны два результата:
- Если загрузчик с указанным идентификатором уже существует, повторно используется последний созданный загрузчик.
- Если загрузчик с указанным идентификатором не существует,
initLoader()вызывает методonCreateLoader()(LoaderManager.LoaderCallbacks). Здесь вы реализуете код для создания экземпляра и возврата нового загрузчика. Подробнее об этом рассказывается в разделеonCreateLoader.
В обоих случаях реализация LoaderManager.LoaderCallbacks связывается с загрузчиком и вызывается при изменении его состояния. Если в момент вызова вызывающий объект находится в состоянии started, а запрашиваемый загрузчик уже существует и сгенерировал данные, система немедленно вызывает onLoadFinished() (в процессе выполнения initLoader()). Вы должны быть готовы к этому. Подробнее об этом обратном вызове рассказывается в разделе
onLoadFinished.
Метод initLoader() возвращает созданный объект Loader, но вам не нужно сохранять ссылку на него. LoaderManager автоматически управляет жизненным циклом загрузчика. LoaderManager запускает и останавливает загрузку, когда это необходимо, и поддерживает состояние загрузчика и связанного с ним контента.
Как следует из этого, вы редко взаимодействуете с загрузчиками напрямую.
Чаще всего методы LoaderManager.LoaderCallbacks используются для вмешательства в процесс загрузки при возникновении определенных событий. Подробнее об этом рассказывается в разделе Использование обратных вызовов LoaderManager.
Как перезапустить загрузчик
При использовании initLoader(), как показано в предыдущем разделе, используется существующий загрузчик с указанным идентификатором, если он есть.
Если такого файла нет, он будет создан. Но иногда вам может потребоваться удалить старые данные и начать заново.
Чтобы удалить старые данные, используйте 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(): создает экземпляр и возвращает новый объектLoaderдля заданного идентификатора.
-
onLoadFinished(): вызывается, когда ранее созданный загрузчик завершает загрузку.
onLoaderReset()– вызывается при сбросе ранее созданного загрузчика, в результате чего его данные становятся недоступными.
Подробные инструкции приведены в следующих разделах.
onCreateLoader
При попытке доступа к загрузчику, например через initLoader(), проверяется, существует ли загрузчик с указанным идентификатором. Если нет, то запускается метод LoaderManager.LoaderCallbacks onCreateLoader(). Здесь вы создаете новый загрузчик. Обычно это CursorLoader, но вы можете реализовать собственный подкласс Loader.
В примере ниже метод обратного вызова onCreateLoader() создает объект 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); }
Пример
Ниже приведен пример полной реализации класса Fragment, который отображает ListView с результатами запроса к поставщику контента контактов. Для управления запросом у поставщика используется 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 – пример того, как использовать троттлинг, чтобы уменьшить количество запросов, которые выполняет поставщик контента при изменении данных.