Создаём ContentProvider для взаимодействия с БД заметок.

Урок 21 — Создаём ContentProvider для взаимодействия с БД заметок.

Что такое ContentProvider?

ContentProvider — механизм передачи данных от одного приложения к другому. Мы с вами уже сталкивались с ним несколькими уроками ранее, когда передавали ссылку на файл.

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

А что, если приложение хранит список контактов пользователя, и другому приложению потребовалось получить доступ к этим данным? Как раз для таких случаев и придуман механизм ContentProvider.

В нашем случае реализация ContentProvider для доступа к БД избыточна (мы не предполагаем предоставления доступа для других приложений), но будет хорошим примером реализации.

Ничего сложного в его реализации нет — это всего лишь класс, в котором мы должны переопределить следующие методы:

  • onCreate() — тут мы создаём БД
  • query() — этот метод вызывается, когда кто-то запрашивает данные из БД
  • insert() — вставляем данные в БД
  • delete() — удаляем данные из БД
  • update() — обновляем данные в БД

Доступ к данным в ContentProvider получается при помощи URI, например такого: content://com.skillberg.notes.provider/notes/1.

  • content:// — схема. Она сообщает, что мы хотим получить доступ к контенту.
  • com.skillberg.notes.provider — так называемый Authority, "адрес" ContentProvider.
  • notes — путь к хранилищу данных определённого типа. Чтобы было понятнее — в большинстве случаев это просто имя таблицы, в которой хранятся данные.
  • 1 — идентификатор контента. Например, значение поля _id в таблице.

Давайте же приступим к реализации!

Создание ContentProvider

Разбираемся с URI

В первую очередь, добавьте две константы в класс NotesContract:

public final class NotesContract {
    public static final String DB_NAME = "notes.db";
    public static final int DB_VERSION = 1;

    public static final String AUTHORITY = "com.skillberg.notes.provider
    public static final String URI = "content://" + AUTHORITY;
    // ...

Первая — authority ContentProvider.

Вторая — базовый URI для доступа к контенту.

Теперь в класс Notes внутри NotesContract добавьте константу, в которой будет храниться URI для доступа к конкретному типу данных — к заметкам:

public static abstract class Notes implements BaseColumns {
    public static final String TABLE_NAME = "notes";
    public static final Uri URI = Uri.parse(NotesContract.URI + "/" + TABLE_NAME);
    // ...

И в него же добавьте константы, обозначающие типы данных:

// Список заметок
public static final String URI_TYPE_NOTE_DIR = "vnd.android.cursor.dir/vnd.skillberg.note";

// Одна заметка
public static final String URI_TYPE_NOTE_ITEM = "vnd.android.cursor.item/vnd.skillberg.note";

Теперь в пакете db создайте новый класс NotesProvider:

public class NotesProvider extends ContentProvider {
    private NotesDbHelper notesDbHelper;
}

Переопределите в нём метод onCreate():

@Override
public boolean onCreate() {
    notesDbHelper = new NotesDbHelper(getContext());
    return true;
}

При обращении к ContentProvider последний должен понимать, о каком типе данных идёт речь. Например, если мы запросим список заметок, то он должен вернуть список, а если конкретную заметку — то одну заметку. Это разные типы данных.

Как он поймёт, о каком типе данных идёт речь? По URI — в нашем случае для списка заметок это будет content://com.skillberg.notes.provider/notes, а для конкретной заметки, скажем, с id = 1content://com.skillberg.notes.provider/notes/1.

В этом ему поможет специальный класс UriMatcher. Он принимает на вход URI и возвращает числовую константу, соответствующую определённому типу данных.

Сначала нужно задать URI и константы типов данных.

Добавьте две константы в NotesProvider:

private static final int NOTES = 1;
private static final int NOTE = 2;

Первая — список заметок, вторая — одна заметка.

Теперь добавьте в него же UriMatcher:

private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);

Единственный параметр конструктора — константа, которую нужно возвращать, если путь после authority не будет задан.

Инициализируем UriMatcher:

static {
    URI_MATCHER.addURI(NotesContract.AUTHORITY, "notes", NOTES);
    URI_MATCHER.addURI(NotesContract.AUTHORITY, "notes/#", NOTE);
}

Первый параметр — authority, второй — путь к данным, третий — константа, которую нужно вернуть, если URI совпадёт.

В первой строчке мы матчим список заметок. Во второй — конкретную заметку # — "плейсхолдер" для id заметки.

Получился вот такой код:

public class NotesProvider extends ContentProvider {
    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);

    private static final int NOTES = 1;
    private static final int NOTE = 2;

    static {
        URI_MATCHER.addURI(NotesContract.AUTHORITY, "notes", NOTES);
        URI_MATCHER.addURI(NotesContract.AUTHORITY, "notes/#", NOTE);
    }

    private NotesDbHelper notesDbHelper;

    @Override
    public boolean onCreate() {
        notesDbHelper = new NotesDbHelper(getContext());
        return true;
    }
    // ...

getType()

Пример места, в котором используется то, что мы сделали выше — метод getType().

Он принимает URI контента в качестве параметра и возвращает тип контента (не числовую константу, а тот, который мы определяли в контракте).

Давайте реализуем этот метод в нашем NotesProvider:

@Nullable
@Override
public String getType(@NonNull Uri uri) {
    switch (URI_MATCHER.match(uri)) {
        case NOTES:
            return NotesContract.Notes.URI_TYPE_NOTE_DIR;
        case NOTE:
            return NotesContract.Notes.URI_TYPE_NOTE_ITEM;
        default:
            return null;
    }
}

Как видите, всё просто.

query()

ContentProvider.query() — метод, использующийся для получения данных, соответствующих определённым критериям.

Он принимает следующие параметры:

  • Uri uriURI контента.
  • String[] projection — массив столбцов, которые нужно выбрать из таблицы. Опциональный параметр.
  • String selection — параметры выборки (условие после WHERE в SQL). Опциональный параметр.
  • String[] selectionArgs — аргументы выборки (будут экранированы). Опциональный параметр.
  • String sortOrder — столбец, по которому сортировать данные и направление сортировки. Опциональный параметр.

Возвращает же он Cursor.

Курсор — специальный объект, позволяющий получать данные из БД "порциями", а не целиком.

Он будет особенно полезен, когда в базе тысячи или сотни тысяч записей (ведь для выборки всех данных за раз банально не хватит памяти), но на мобильных устройствах он нужен всегда из-за ограниченности ресурсов.

Итак, переопределите метод query():

@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
                    @Nullable String[] selectionArgs, @Nullable String sortOrder) {
}

В первую очередь в нём нужно получить инстанс базы данных:

Обратите внимание: поскольку мы будем использовать БД только для чтения, используем метод getReadableDatabase(). Метод getWritableDatabase() мы будем использовать только когда нам нужно будет что-то записать в базу.

Поскольку метод query() будет использоваться для доступа ко всей БД заметок, нужно понять, с каким именно типом данных работаем:

switch (URI_MATCHER.match(uri)) {
    case NOTES:
        break;

    case NOTE:
        break;

    default:
        return null;
}

В случае, если нам нужен список заметок, нужно задать параметры сортировки (если они не заданы):

if (TextUtils.isEmpty(sortOrder)) {
    sortOrder = NotesContract.Notes.COLUMN_UPDATED_TS + " DESC";
}

То есть, по умолчанию сортируем по убыванию столбца "последнее обновление".

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

return db.query(NotesContract.Notes.TABLE_NAME,
        projection,
        selection,
        selectionArgs,
        null, // groupBy, не используется
        null, // having, не используется
        sortOrder);

В случае выборки одной заметки делаем то же самое, за исключением сортировки:

return db.query(NotesContract.Notes.TABLE_NAME,
        projection,
        selection,
        selectionArgs,
        null,
        null,
        sortOrder);

В итоге, метод будет таким:

@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
                    @Nullable String[] selectionArgs, @Nullable String sortOrder) {
    SQLiteDatabase db = notesDbHelper.getReadableDatabase();
    switch (URI_MATCHER.match(uri)) {
        case NOTES:
            if (TextUtils.isEmpty(sortOrder)) {
                sortOrder = NotesContract.Notes.COLUMN_UPDATED_TS + " DESC";
            }
            return db.query(NotesContract.Notes.TABLE_NAME,
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null,
                    sortOrder);
        case NOTE:
            String id = uri.getLastPathSegment();

                if (TextUtils.isEmpty(selection)) {
                    selection = NotesContract.Notes._ID + " = ?";
                    selectionArgs = new String[]{id};
                } else {
                    selection = selection + " AND " + NotesContract.Notes._ID + " = ?";

                    String[] newSelectionArgs = new String[selectionArgs.length + 1];

                    System.arraycopy(selectionArgs, 0, newSelectionArgs, 0, selectionArgs.length);

                    newSelectionArgs[newSelectionArgs.length - 1] = id;

                    selectionArgs = newSelectionArgs; 
                }

        default:
            return null;
    }
}

В случае, если была запрошена конкретная заметка, мы получаем её идентификатор, используя метод getLastPathSegment().

После этого проверяем, не задан ли аргумент параметров выборки. Если не задан, то всё просто: создаём его и аргументы выборки.

Если же задан — дополняем его идентификатором, а так же копируем переданные ранее аргументы выборки и добавляем к ним полученный идентификатор.

insert()

Метод insert(), что логично, отвечает за вставку данных в БД.

Он принимает всего два параметра:

  • Uri uri — URI, как и в query.
  • ContentValues contentValues — Набор пар "ключ - значение" ("столбец - данные").

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

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


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


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

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

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