Погрузчики

Загрузчики устарели начиная с Android 9 (уровень API 28). Рекомендуемый вариант загрузки данных при обработке жизненных циклов Activity и Fragment — использовать комбинацию объектов ViewModel и LiveData . Модели представлений выдерживают изменения конфигурации, как и загрузчики, но с меньшим количеством шаблонного кода. LiveData предоставляет способ загрузки данных с учетом жизненного цикла, который можно повторно использовать в нескольких моделях представления. Вы также можете объединить LiveData с помощью MediatorLiveData . Любые наблюдаемые запросы, например запросы к базе данных Room , можно использовать для наблюдения за изменениями данных.

ViewModel и LiveData также доступны в ситуациях, когда у вас нет доступа к LoaderManager , например в Service . Использование этих двух компонентов в тандеме обеспечивает простой способ доступа к данным, необходимым вашему приложению, без необходимости иметь дело с жизненным циклом пользовательского интерфейса. Чтобы узнать больше о LiveData , см. обзор LiveData . Чтобы узнать больше о ViewModel , смотрите обзор ViewModel .

API-интерфейс Loader позволяет загружать данные из поставщика контента или другого источника данных для отображения в FragmentActivity или Fragment .

Без загрузчиков вы можете столкнуться со следующими проблемами:

  • Если вы извлекаете данные непосредственно в действии или фрагменте, ваши пользователи страдают от недостаточной реакции из-за выполнения потенциально медленных запросов из потока пользовательского интерфейса.
  • Если вы извлекаете данные из другого потока, возможно, с помощью AsyncTask , то вы несете ответственность за управление как этим потоком, так и потоком пользовательского интерфейса посредством различных событий жизненного цикла активности или фрагмента, таких как onDestroy() и изменений конфигурации.

Погрузчики решают эти проблемы и обладают другими преимуществами:

  • Загрузчики работают в отдельных потоках, чтобы предотвратить медленный или не отвечающий на запросы пользовательский интерфейс.
  • Загрузчики упрощают управление потоками, предоставляя методы обратного вызова при возникновении событий.
  • Загрузчики сохраняются и кэшируют результаты при изменении конфигурации, чтобы предотвратить дублирование запросов.
  • Загрузчики могут реализовать наблюдателя для отслеживания изменений в базовом источнике данных. Например, CursorLoader автоматически регистрирует ContentObserver , чтобы инициировать перезагрузку при изменении данных.

Обзор API загрузчика

Существует несколько классов и интерфейсов, которые могут быть задействованы при использовании загрузчиков в приложении. Они сведены в следующую таблицу:

Класс/Интерфейс Описание
LoaderManager Абстрактный класс, связанный с FragmentActivity или Fragment для управления одним или несколькими экземплярами Loader . Для каждого действия или фрагмента существует только один LoaderManager , но LoaderManager может управлять несколькими загрузчиками.

Чтобы получить LoaderManager , вызовите getSupportLoaderManager() из активности или фрагмента.

Чтобы начать загрузку данных из загрузчика, вызовите initLoader() или restartLoader() . Система автоматически определяет, существует ли уже загрузчик с таким целочисленным идентификатором, и либо создает новый загрузчик, либо повторно использует существующий загрузчик.

LoaderManager.LoaderCallbacks Этот интерфейс содержит методы обратного вызова, которые вызываются при возникновении событий загрузчика. Интерфейс определяет три метода обратного вызова:
  • onCreateLoader(int, Bundle) : вызывается, когда системе необходимо создать новый загрузчик. В своем коде создайте объект Loader и верните его в систему.
  • onLoadFinished(Loader<D>, D) : вызывается, когда загрузчик завершил загрузку данных. Обычно вы отображаете данные пользователю в своем коде.
  • onLoaderReset(Loader<D>) : вызывается, когда ранее созданный загрузчик сбрасывается, когда вы вызываете destroyLoader(int) или когда действие или фрагмент уничтожаются, что делает его данные недоступными. Удалите из своего кода все ссылки на данные загрузчика.
Ваша активность или фрагмент обычно реализует этот интерфейс, и он регистрируется при вызове initLoader() или restartLoader() .
Loader Загрузчики выполняют загрузку данных. Этот класс является абстрактным и служит базовым классом для всех загрузчиков. Вы можете напрямую создать подкласс Loader или использовать один из следующих встроенных подклассов для упрощения реализации:

В следующих разделах показано, как использовать эти классы и интерфейсы в приложении.

Используйте загрузчики в приложении

В этом разделе описывается, как использовать загрузчики в приложении Android. Приложение, использующее загрузчики, обычно включает в себя следующее:

  • FragmentActivity или Fragment .
  • Экземпляр LoaderManager .
  • CursorLoader для загрузки данных, поддерживаемых ContentProvider . Альтернативно вы можете реализовать свой собственный подкласс Loader или AsyncTaskLoader для загрузки данных из другого источника.
  • Реализация LoaderManager.LoaderCallbacks . Здесь вы создаете новые загрузчики и управляете ссылками на существующие загрузчики.
  • Способ отображения данных загрузчика, например SimpleCursorAdapter .
  • Источник данных, например ContentProvider , при использовании CursorLoader .

Запустить загрузчик

LoaderManager управляет одним или несколькими экземплярами Loader внутри FragmentActivity или Fragment . Для каждого действия или фрагмента существует только один LoaderManager .

Обычно вы инициализируете Loader в методе onCreate() действия или методе onCreate() фрагмента. Вы делаете это следующим образом:

Котлин

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() принимает следующие параметры:

  • Уникальный идентификатор, идентифицирующий загрузчик. В этом примере идентификатор равен 0 .
  • Необязательные аргументы, которые необходимо передать загрузчику при создании (в данном примере null ).
  • Реализация LoaderManager.LoaderCallbacks , которую LoaderManager вызывает для сообщения о событиях загрузчика. В этом примере локальный класс реализует интерфейс LoaderManager.LoaderCallbacks , поэтому он передает ссылку на себя, this .

Вызов initLoader() гарантирует, что загрузчик инициализирован и активен. У него есть два возможных результата:

  • Если загрузчик, указанный идентификатором, уже существует, повторно используется последний созданный загрузчик.
  • Если загрузчик, указанный идентификатором, не существует, initLoader() запускает метод LoaderManager.LoaderCallbacks onCreateLoader() . Здесь вы реализуете код для создания экземпляра и возврата нового загрузчика. Дополнительную информацию см. в разделе onCreateLoader .

В любом случае данная реализация LoaderManager.LoaderCallbacks связана с загрузчиком и вызывается при изменении состояния загрузчика. Если в момент этого вызова вызывающий объект находится в запущенном состоянии, а запрошенный загрузчик уже существует и сгенерировал свои данные, то система немедленно вызывает onLoadFinished() во время initLoader() . Вы должны быть готовы к тому, что это произойдет. Более подробное обсуждение этого обратного вызова см. в разделе onLoadFinished .

Метод initLoader() возвращает созданный Loader , но вам не нужно фиксировать ссылку на него. LoaderManager автоматически управляет жизнью загрузчика. LoaderManager запускает и останавливает загрузку при необходимости и поддерживает состояние загрузчика и связанного с ним содержимого.

Из этого следует, что вы редко взаимодействуете с загрузчиками напрямую. Чаще всего методы LoaderManager.LoaderCallbacks используются для вмешательства в процесс загрузки при возникновении определенных событий. Более подробное обсуждение этой темы см. в разделе «Использование обратных вызовов LoaderManager» .

Перезапустите загрузчик

Когда вы используете initLoader() , как показано в предыдущем разделе, он использует существующий загрузчик с указанным идентификатором, если таковой имеется. Если его нет, он создает его. Но иногда вам хочется отказаться от старых данных и начать все сначала.

Чтобы удалить старые данные, используйте restartLoader() . Например, следующая реализация SearchView.OnQueryTextListener перезапускает загрузчик при изменении запроса пользователя. Загрузчик необходимо перезапустить, чтобы он мог использовать обновленный фильтр поиска для выполнения нового запроса.

Котлин

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() : создает экземпляр и возвращает новый Loader для данного идентификатора.
  • onLoadFinished() : вызывается, когда ранее созданный загрузчик завершил загрузку.
  • onLoaderReset() : вызывается, когда ранее созданный загрузчик сбрасывается, что делает его данные недоступными.

Более подробно эти методы описаны в следующих разделах.

onCreateLoader

Когда вы пытаетесь получить доступ к загрузчику, например, через initLoader() , он проверяет, существует ли загрузчик, указанный идентификатором. Если этого не происходит, он запускает метод LoaderManager.LoaderCallbacks onCreateLoader() . Здесь вы создаете новый загрузчик. Обычно это CursorLoader , но вы можете реализовать свой собственный подкласс Loader .

В следующем примере метод обратного вызова onCreateLoader() создает CursorLoader используя его метод-конструктор, которому требуется полный набор информации, необходимой для выполнения запроса к ContentProvider . В частности, для этого необходимо следующее:

  • uri : URI для извлекаемого контента.
  • проекция : список столбцов, которые нужно вернуть. При передаче null возвращаются все столбцы, что неэффективно.
  • выбор : фильтр, объявляющий, какие строки возвращать, в формате предложения SQL WHERE (за исключением самого WHERE). При передаче null возвращаются все строки для данного URI.
  • SelectionArgs : если вы включаете ?s в выборку, они заменяются значениями из selectedArgs в том порядке, в котором они появляются в выборке. Значения связаны как строки.
  • sortOrder : как упорядочить строки, отформатированные как предложение SQL ORDER BY (за исключением самого ORDER BY). При передаче null используется порядок сортировки по умолчанию, который может быть неупорядоченным.

Котлин

// 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 не закрывался, как показано в следующем примере:

Котлин

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 :

Котлин

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);
}

Пример

В качестве примера приведена полная реализация Fragment , который отображает ListView , содержащий результаты запроса к поставщику контента контактов. Он использует CursorLoader для управления запросом к поставщику.

Поскольку этот пример относится к приложению для доступа к контактам пользователя, его манифест должен включать разрешение READ_CONTACTS .

Котлин

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);
    }
}

Больше примеров

Следующие примеры иллюстрируют, как использовать загрузчики:

  • LoaderCursor : полная версия предыдущего фрагмента.
  • Получение списка контактов : пошаговое руководство, использующее CursorLoader для получения данных от поставщика контактов.
  • LoaderThrottle : пример использования регулирования для уменьшения количества запросов, выполняемых поставщиком контента при изменении его данных.