Loaders. CursorLoader. Работаем с ContentProvider, используя загрузчики.

Урок 22 — Loaders. CursorLoader. Работаем с ContentProvider, используя загрузчики.

Loaders

Loader (загрузчик) — специальный механизм, появившийся в далёком Android 3.0, и позволяющий без труда загружать данные в фоне.

Возможность фоновой загрузки, однако, не единственное преимущество загрузчиков.

Помимо этого они отслеживают источник данных, автоматически подгружая изменения, а так же избавляют нас от уймы лишнего шаблонного кода.

CursorLoader

Давайте перепишем метод MainActivity.select() из прошлого урока, используя загрузчики.

В Android SDK есть класс CursorLoader, созданный специально для работы с БД, в первую очередь — с ContentProvider.

Давайте же разберёмся, как работать с загрузчиками.

В MainActivity в конце метода onCreate() напишите (весь написанный ранее код можно удалить):

getLoaderManager().initLoader(
        0, // Идентификатор загрузчика
        null, // Аргументы
        this // Callback для событий загрузчика
);
  • Идентификатор загрузчика нужен для того, чтобы LoaderManager мог понять, о каком конкретно загрузчике идёт речь.
  • Аргументы опциональны, и будут переданы в загрузчик при создании. В нашем случае они не нужны.
  • Callback нужен для получения событий загрузчика.

На последнем аргументе нужно остановиться поподробнее.

Имеющийся на данный момент код не будет собираться, так как в качестве коллбэка мы передали Activity, а она не реализует интерфейс LoaderManager.LoaderCallbacks.

Давайте реализуем его и необходимые методы. В результате получим такой код:

public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getLoaderManager().initLoader(
                0, // Идентификатор загрузчика
                null, // Аргументы
                this // Callback для событий загрузчика
        );
    }


    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return null;
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {

    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {

    }

}

Что происходит, когда мы вызываем метод LoaderManager.initLoader()? LoaderManager смотрит, не существует ли уже загрузчик с заданным идентификатором.

Если существует — то использует его. Если же не существует, то создаёт новый Loader, вызывая метод onCreateLoader() из коллбэка, то есть создать загрузчик должны мы сами.

Давайте сделаем это:

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    return new CursorLoader(
            this,  // Контекст
            NotesContract.Notes.URI, // URI
            NotesContract.Notes.LIST_PROJECTION, // Столбцы
            null, // Параметры выборки
            null, // Аргументы выборки
            null // Сортировка по умолчанию
    );
}

Как видите, единственное отличие от ContentResolver в параметрах — контекст.

onLoadFinished

Этот метод вызовется, когда Loader закончит загружать данные. Мы работаем с Cursor, поэтому в качестве параметра будет передан курсор.

Давайте, как и в прошлом уроке, выведем в лог количество записей в таблице:

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    Log.i("Test", "Load finished: " + cursor.getCount());
}

Как вы могли заметить, закрывать Cursor не нужно — CursorLoader сделает это за нас!

Запустите приложение, и вы увидите в логах примерно такой текст:

10-16 23:03:48.644 5047-5047/? I/Test: Load finished: 7

onLoaderReset

Во многих случаях вам может потребоваться "сбросить" загрузчик, чтобы создать новый, который загрузит другие данные. Соответственно, старые данные станут неактуальными вскоре после сброса загрузчика.

Именно в этот момент и вызывается метод onLoaderReset(). Он говорит о том, что данные уже неактуальны, и нужно перестать их использовать. В нашем случае метод пока что не используется, так что оставим его пустым.

В итоге получим вот такой код:

public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getLoaderManager().initLoader(
                0, // Идентификатор загрузчика
                null, // Аргументы
                this // Callback для событий загрузчика
        );
    }


    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return new CursorLoader(
                this,  // Контекст
                NotesContract.Notes.URI, // URI
                NotesContract.Notes.LIST_PROJECTION, // Столбцы
                null, // Параметры выборки
                null, // Аргументы выборки
                null // Сортировка по умолчанию
        );
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        Log.i("Test", "Load finished: " + cursor.getCount());
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
    }


}

CursorRecyclerAdapter

Как вы помните, для отображения данных в RecyclerView необходим адаптер. Однако, обычный адаптер нам тут не особо поможет — нужно создать адаптер, который будет работать с Cursor в качестве источника данных.

Давайте реализуем его. Создайте пакет ui, а в нём класс CursorRecyclerAdapter:

CursorRecyclerAdapter в пакете ui
CursorRecyclerAdapter в пакете ui

Не забудьте добавить библиотеку с RecyclerView в зависимости build.gradle, находящийся в модуле app, например, вот так: implementation 'com.android.support:recyclerview-v7:26.1.0'

Сам класс будет аж вот таким (для начала):

public abstract class CursorRecyclerAdapter<ViewHolder extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<ViewHolder> {

    protected Cursor cursor; // Курсор
    protected boolean isDataValid; // Валидны ли данные
    protected int idColumnIndex; // Индекс столбца ID в курсоре

    public CursorRecyclerAdapter(Cursor cursor) {
        super();

        this.cursor = cursor;

        // Данные корректны если курсор не null
        isDataValid = cursor != null;

        // Пытаемся получить индекс столбца ID, если курсор не null, в ином случае -1
        idColumnIndex = cursor != null
                ? cursor.getColumnIndexOrThrow(NotesContract.Notes._ID)
                : -1;

        // Каждый элемент имеет уникальный ID
        setHasStableIds(true);
    }
}

Когда мы работаем с курсором, мы двигаемся от строчки к строчке и получаем значения каждого столбца по индексу этого столбца.

Например, если столбец _id идёт первым, то его индекс будет равен 0, и обращаться к нему мы будем используя индекс 0, а не имя.

Чтобы узнать индекс столбца, нужно вызвать метод getColumnIndex() или getColumnIndexOrThrow(), передав в него имя столбца.

Единственное различие между этими двумя методами в том, что первый в случае отсутствия искомого столбца вернёт -1, а второй бросит исключение IllegalArgumentException.

Поскольку теперь мы взаимодействуем не просто с массивом данных, а с Cursor, создадим новый метод:

public abstract void onBindViewHolder(ViewHolder viewHolder, Cursor cursor);

Он будет вызываться в стандартном onBindViewHolder(), который мы переопределим:

@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
    // Если данные некорректны — кидаем исключение
    if (!isDataValid) {
        throw new IllegalStateException("Cursor is not valid!");
    }

    // Попробовали перейти к определённой строке, но это не получилось
    if (!cursor.moveToPosition(position)) {
        throw new IllegalStateException("Can not move to position " + position);
    }

    // Вызываем новый метод
    onBindViewHolder(viewHolder, cursor);
}

В переопределённом методе добавляем несколько проверок и вызываем новый метод.

Обратите внимание, что ранее, когда мы работали с адаптерами, в методе onBindViewHolder() мы просто брали элемент из списка по индексу и взаимодействовали с ним. С курсорами это выглядит немного иначе.

Когда нам нужно получить доступ к определённой строке в курсоре, мы должны вызвать метод Cursor.moveToPosition(). В случае, если сделать это не удалось (например, количество строк в курсоре меньше, чем индекс строки, который мы передали), метод вернёт false.

Собственно, после этого мы можем получать значения из курсора.

Следующий шаг — переопределим метод getItemCount(). В нём мы просто вернём количество строк в курсоре (если он есть):

@Override
public int getItemCount() {
    if (isDataValid && cursor != null) {
        return cursor.getCount();
    } else {
        return 0;
    }
}

Далее — getItemId(). Поскольку у каждого элемента в RecyclerView будет свой уникальный идентификатор, мы вызывали setHasStableIds(true) в конструкторе, и теперь должны переопределить метод, который возвращает идентификатор для элемента по его позиции.

Это достаточно просто:

@Override
public long getItemId(int position) {

    // Если с данными всё хорошо и есть курсор
    if (isDataValid && cursor != null) {

        // Если смогли найти нужную строку в курсоре
        if (cursor.moveToPosition(position)) {

            // Возвращаем значение столбца ID
            return cursor.getLong(idColumnIndex);
        }
    }

    // Во всех остальных случаях возвращаем дефолтное значение
    return RecyclerView.NO_ID;
}

Последний шаг — реализовать метод swapCursor(), который будет заменять старый курсор на новый (например, когда мы сбросили лоадер и загрузили данные заново).

@Nullable
public Cursor swapCursor(Cursor newCursor) {
    // Если курсор не изменился — ничего не заменяем
    if (newCursor == this.cursor) {
        return null;
    }

    Cursor oldCursor = this.cursor;
    this.cursor = newCursor;

    if (newCursor != null) {
        idColumnIndex = newCursor.getColumnIndexOrThrow(NotesContract.Notes._ID);
        isDataValid = true;
        notifyDataSetChanged();
    } else {
        idColumnIndex = -1;
        isDataValid = false;
        // Сообщаем, что данных в адаптере больше нет
        notifyItemRangeRemoved(0, getItemCount());
    }
    return oldCursor;

}

Итоговый код вы можете посмотреть по ссылке в конце урока.

View для заметок

Теперь давайте создадим лэйаут для отображения элементов списка заметок. Назовите его view_item_note. Лэйаут будет очень простым: два TextView, в верхнем — заголовок заметки, в нижнем — дата последнего изменения.


Продолжение доступно на платных тарифах

Это недорого — всего от 440 ₽ в месяц!


ВЫБРАТЬ ТАРИФ


Продолжение доступно после регистрации

Все уроки на сайте доступны абсолютно бесплатно после регистрации.

Регистрация займёт меньше минуты ;)