Освой программирование играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Кошкин дом. Контент-провайдер. Часть третья

Создание контент-провайдера

Во второй части мы использовали SQLite. Переделаем пример с использованием контент-провайдера.

В первую очередь контент-провайдер предназначен для того, чтобы другие приложения могли использовать вашу базу данных. На таком принципе построены многие системные приложения. Например, контакты или календарь. Мы можем подключиться к базе контактов, извлечь необходимую информацию и создать своё собственное приложение с использованием полученных данных.

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

Контент-провайдер можно рассматривать как посредник между компонентами приложения, которые вносят данные и самой базой данных. Контент-провайдер берёт на себя обработку данных, следя за правильностью данных. Кроме того, данные могут содержаться не только в базе данных SQLite, но и в других типах базы данных, в файлах и в других источниках. В любом случае приложение не должно переписываться при изменении источника данных. Провайдер берёт всё на себя.

При работе с контент-провайдером вы увидите, что многие методы дублируются. Так же как и с SQLite вам нужно реализовать методы query(), insert(), update(), delete().

Создание контент-провайдера

В пакете data создаём новый класс GuestProvider на основе ContentProvider. Студия предложит добавить обязательные методы.

Method of ContentProvider

Далее пишется очень много кода.

Главное - у провайдера есть основые методы: onCreate(), query(), insert(), update(), delete(), getType(). Часть есть методов выполняют ту же задачу, что и одноимённые методы в SQLiteDatabase.


package ru.alexanderklimov.cathouse.data;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.util.Log;

import ru.alexanderklimov.cathouse.data.HotelContract.GuestEntry;

import static ru.alexanderklimov.cathouse.data.HotelDbHelper.LOG_TAG;


public class GuestProvider extends ContentProvider {

    public static final String TAG = GuestProvider.class.getSimpleName();

    /**
     * URI matcher code for the content URI for the guests table
     */
    private static final int GUESTS = 100;

    /**
     * URI matcher code for the content URI for a single guest in the guests table
     */
    private static final int GUEST_ID = 101;

    /**
     * UriMatcher object to match a content URI to a corresponding code.
     * The input passed into the constructor represents the code to return for the root URI.
     * It's common to use NO_MATCH as the input for this case.
     */
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    // Static initializer. This is run the first time anything is called from this class.
    static {
        // The calls to addURI() go here, for all of the content URI patterns that the provider
        // should recognize. All paths added to the UriMatcher have a corresponding code to return
        // when a match is found.

        // The content URI of the form "content://com.example.android.guests/guests" will map to the
        // integer code {@link #GUESTS}. This URI is used to provide access to MULTIPLE rows
        // of the guests table.
        sUriMatcher.addURI(HotelContract.CONTENT_AUTHORITY, HotelContract.PATH_GUESTS, GUESTS);

        // The content URI of the form "content://com.example.android.guests/guests/#" will map to the
        // integer code {@link #GUEST_ID}. This URI is used to provide access to ONE single row
        // of the guests table.
        //
        // In this case, the "#" wildcard is used where "#" can be substituted for an integer.
        // For example, "content://com.example.android.guests/guests/3" matches, but
        // "content://com.example.android.guests/guests" (without a number at the end) doesn't match.
        sUriMatcher.addURI(HotelContract.CONTENT_AUTHORITY, HotelContract.PATH_GUESTS + "/#", GUEST_ID);
    }

    /**
     * Database helper object
     */
    private HotelDbHelper mDbHelper;

    @Override
    public boolean onCreate() {
        mDbHelper = new HotelDbHelper(getContext());
        return true;
    }

    @Nullable
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // Получим доступ к базе данных для чтения
        SQLiteDatabase database = mDbHelper.getReadableDatabase();

        // Курсор, содержащий результат запроса
        Cursor cursor;

        // Figure out if the URI matcher can match the URI to a specific code
        int match = sUriMatcher.match(uri);
        switch (match) {
            case GUESTS:
                // For the GUESTS code, query the guests table directly with the given
                // projection, selection, selection arguments, and sort order. The cursor
                // could contain multiple rows of the guests table.
                cursor = database.query(GuestEntry.TABLE_NAME, projection, selection, selectionArgs,
                        null, null, sortOrder);
                break;
            case GUEST_ID:
                // For the GUEST_ID code, extract out the ID from the URI.
                // For an example URI such as "content://com.example.android.guests/guests/3",
                // the selection will be "_id=?" and the selection argument will be a
                // String array containing the actual ID of 3 in this case.
                //
                // For every "?" in the selection, we need to have an element in the selection
                // arguments that will fill in the "?". Since we have 1 question mark in the
                // selection, we have 1 String in the selection arguments' String array.
                selection = GuestEntry._ID + "=?";
                selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};

                // This will perform a query on the guests table where the _id equals 3 to return a
                // Cursor containing that row of the table.
                cursor = database.query(GuestEntry.TABLE_NAME, projection, selection, selectionArgs,
                        null, null, sortOrder);
                break;
            default:
                throw new IllegalArgumentException("Cannot query unknown URI " + uri);
        }
        return cursor;
    }

    @Nullable
    @Override
    public String getType(Uri uri) {
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case GUESTS:
                return GuestEntry.CONTENT_LIST_TYPE;
            case GUEST_ID:
                return GuestEntry.CONTENT_ITEM_TYPE;
            default:
                throw new IllegalStateException("Unknown URI " + uri + " with match " + match);
        }
    }

    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case GUESTS:
                return insertGuest(uri, values);
            default:
                throw new IllegalArgumentException("Insertion is not supported for " + uri);
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // Get writeable database
        SQLiteDatabase database = mDbHelper.getWritableDatabase();

        final int match = sUriMatcher.match(uri);
        switch (match) {
            case GUESTS:
                // Delete all rows that match the selection and selection args
                return database.delete(GuestEntry.TABLE_NAME, selection, selectionArgs);
            case GUEST_ID:
                // Delete a single row given by the ID in the URI
                selection = GuestEntry._ID + "=?";
                selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
                return database.delete(GuestEntry.TABLE_NAME, selection, selectionArgs);
            default:
                throw new IllegalArgumentException("Deletion is not supported for " + uri);
        }
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case GUESTS:
                return updateGuest(uri, values, selection, selectionArgs);
            case GUEST_ID:
                // For the GUEST_ID code, extract out the ID from the URI,
                // so we know which row to update. Selection will be "_id=?" and selection
                // arguments will be a String array containing the actual ID.
                selection = GuestEntry._ID + "=?";
                selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
                return updateGuest(uri, values, selection, selectionArgs);
            default:
                throw new IllegalArgumentException("Update is not supported for " + uri);
        }
    }

    /**
     * Insert a guest into the database with the given content values. Return the new content URI
     * for that specific row in the database.
     */
    private Uri insertGuest(Uri uri, ContentValues values) {
        // Check that the name is not null
        String name = values.getAsString(GuestEntry.COLUMN_NAME);
        if (name == null) {
            throw new IllegalArgumentException("Guest requires a name");
        }

        // Check that the gender is valid
        Integer gender = values.getAsInteger(GuestEntry.COLUMN_GENDER);
        if (gender == null || !GuestEntry.isValidGender(gender)) {
            throw new IllegalArgumentException("Guest requires valid gender");
        }

        // If the age is provided, check that it's greater than or equal to 0 kg
        Integer age = values.getAsInteger(GuestEntry.COLUMN_AGE);
        if (age != null && age < 0) {
            throw new IllegalArgumentException("Guest requires valid age");
        }

        // No need to check the city, any value is valid (including null).

        // Get writeable database
        SQLiteDatabase database = mDbHelper.getWritableDatabase();

        // Insert the new guest with the given values
        long id = database.insert(GuestEntry.TABLE_NAME, null, values);
        // If the ID is -1, then the insertion failed. Log an error and return null.
        if (id == -1) {
            Log.e(TAG, "Failed to insert row for " + uri);
            return null;
        }

        // Return the new URI with the ID (of the newly inserted row) appended at the end
        return ContentUris.withAppendedId(uri, id);
    }

    /**
     * Update guests in the database with the given content values. Apply the changes to the rows
     * specified in the selection and selection arguments (which could be 0 or 1 or more guests).
     * Return the number of rows that were successfully updated.
     */
    private int updateGuest(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        // If the {@link GuestEntry#COLUMN_NAME} key is present,
        // check that the name value is not null.
        if (values.containsKey(GuestEntry.COLUMN_NAME)) {
            String name = values.getAsString(GuestEntry.COLUMN_NAME);
            if (name == null) {
                throw new IllegalArgumentException("Guest requires a name");
            }
        }

        // If the {@link GuestEntry#COLUMN_GENDER} key is present,
        // check that the gender value is valid.
        if (values.containsKey(GuestEntry.COLUMN_GENDER)) {
            Integer gender = values.getAsInteger(GuestEntry.COLUMN_GENDER);
            if (gender == null || !GuestEntry.isValidGender(gender)) {
                throw new IllegalArgumentException("Guest requires valid gender");
            }
        }

        // If the {@link GuestEntry#COLUMN_AGE} key is present,
        // check that the age value is valid.
        if (values.containsKey(GuestEntry.COLUMN_AGE)) {
            // Check that the age is greater than or equal to 0
            Integer age = values.getAsInteger(GuestEntry.COLUMN_AGE);
            if (age != null && age < 0) {
                throw new IllegalArgumentException("Guest requires valid age");
            }
        }

        // No need to check the breed, any value is valid (including null).

        // If there are no values to update, then don't try to update the database
        if (values.size() == 0) {
            return 0;
        }

        // Otherwise, get writeable database to update the data
        SQLiteDatabase database = mDbHelper.getWritableDatabase();

        // Returns the number of database rows affected by the update statement
        return database.update(GuestEntry.TABLE_NAME, values, selection, selectionArgs);
    }
}

В методе query() формируется запрос в зависимости от того, что мы хотим получить - информацию о всей таблице или только об одном ряде через switch. Первый аргумент метода uri поможет узнать наше намерение. Метод sUriMatcher.match(uri) вернёт нам нужную константу для дальнейшей работы. Остальные аргументы нам уже знакомы.

Также надо добавить информацию в манифест.


<provider
    android:name=".data.GuestProvider"
    android:authorities="ru.alexanderklimov.cathouse"
    android:exported="false" />

Теперь запросы к базе данных делаем не напрямую, а через посредник, которым является созданный провайдер. Обращение к провайдеру происходит через Content Resolver.

Адрес всегда должен начинаться с content://, очень похоже на http: и указывает на схему.

Далее идёт адрес для Content Authority, который обычно совпадает с пакетом вашего приложения. Он прописывается в манифесте (см. выше). Добавляем константу в класс HotelContract (см. ниже код).

Соединяем строку content:// с созданной константой в виде отдельной константы. Для работы с полученной строкой в виде Uri применим метод parse(), который как раз и возвратит объект Uri (см. ниже код).

Третья часть после слеша - это сами данные. В большинстве случаев это таблица вашей базы данных. Добавим ещё одну константу, которая будет присоединяться к предыдущему Uri (см. ниже код).

В классе GuestEntry добавляем дополнительную константу, которая соединяет все три части (см. ниже код).

После имени таблицы может идти четвёртая часть - один ряд из таблицы, который нам нужен для получения детальной информации, например, о госте. Для доступа к ряду таблицы можно использовать его идентификатор _id.

Таким образом адрес контент-провайдера нашего приложения: content://ru.alexanderklimov.cathouse/guests.

Информация о втором госте: content://ru.alexanderklimov.cathouse/guests/2

Два указанных случая - это адреса Uri. Если нам нужно получить список всех гостей таблицы, то мы используем первый Uri без использования идентификатора. Соответственно для отдельного гостя используем второй Uri.

Внесём изменения в класс HotelContract.


// Content Authority
public static final String CONTENT_AUTHORITY = "ru.alexanderklimov.cathouse";

// Создаём объект Uri
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);

// Данные. Обычно имя таблицы
public static final String PATH_GUESTS = "guests";

// В классе GuestEntry
// Соединяем все компоненты адреса
public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, PATH_GUESTS);

Вернёмся в класс MainActivity и перепишем метод displayDatabaseInfo().

Создавать и открывать базу данных теперь нет необходимости, это сделает контент-провайдер. Закомментируем строчу.

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


// Делаем запрос
/*
Cursor cursor = db.query(
        GuestEntry.TABLE_NAME,   // таблица
        projection,            // столбцы
        null,                  // столбцы для условия WHERE
        null,                  // значения для условия WHERE
        null,                  // Don't group the rows
        null,                  // Don't filter by row groups
        null);                   // порядок сортировки
 */
 
Cursor cursor = getContentResolver().query(
        GuestEntry.CONTENT_URI, 
        projection, 
        null, 
        null, 
        null);

Метод query() отличается только первым параметром, вместо имени таблицы мы используем адрес таблицы.

Вернёмся в класс контент-провайдера. Метод insert() использует два параметра. Первый - URI также предназначен для выбора задачи, но вставка новой записи нужна только для всей таблицы, поэтому оператор switch будет состоять из одного варианта с константой GUESTS, вариант GUEST_ID не понадобится.

В классе MainActivity мы можем теперь отредактировать метод вставки новой записи в таблицу через контент-провайдер, убрав вызов базы данных и добавив и заменив вызов метода SQLiteDatabase.insert() на getContentResolver().insert().


private void insertGuest() {

    // Получим доступ к базе данных для записи
    //SQLiteDatabase db = mDbHelper.getWritableDatabase();

    // Создаем объект ContentValues, где имена столбцов ключи,
    // а информация о госте является значениями ключей
    ContentValues values = new ContentValues();
    values.put(GuestEntry.COLUMN_NAME, "Мурзик");
    values.put(GuestEntry.COLUMN_CITY, "Мурманск");
    values.put(GuestEntry.COLUMN_GENDER, GuestEntry.GENDER_MALE);
    values.put(GuestEntry.COLUMN_AGE, 7);

    //long newRowId = db.insert(GuestEntry.TABLE_NAME, null, values);

    Uri newUri = getContentResolver().insert(GuestEntry.CONTENT_URI, values);
}

Аналогично поправим метод в EditorActivity:


private void insertGuest() {
    // Считываем данные из текстовых полей
    String name = mNameEditText.getText().toString().trim();
    String city = mCityEditText.getText().toString().trim();
    String ageString = mAgeEditText.getText().toString().trim();
    int age = Integer.parseInt(ageString);

    //HotelDbHelper mDbHelper = new HotelDbHelper(this);

    //SQLiteDatabase db = mDbHelper.getWritableDatabase();

    ContentValues values = new ContentValues();
    values.put(GuestEntry.COLUMN_NAME, name);
    values.put(GuestEntry.COLUMN_CITY, city);
    values.put(GuestEntry.COLUMN_GENDER, mGender);
    values.put(GuestEntry.COLUMN_AGE, age);

    // Вставляем новый ряд в базу данных и запоминаем его идентификатор
    //long newRowId = db.insert(GuestEntry.TABLE_NAME, null, values);

    // Встравляем новый ряд в провайдер, возвращая URI для нового гостя.
    Uri newUri = getContentResolver().insert(GuestEntry.CONTENT_URI, values);

    // Выводим сообщение в успешном случае или при ошибке
    // if (newRowId == -1) {
    //     // Если ID  -1, значит произошла ошибка
    //     Toast.makeText(this, "Ошибка при заведении гостя", Toast.LENGTH_SHORT).show();
    // } else {
    //     Toast.makeText(this, "Гость заведён под номером: " + newRowId, Toast.LENGTH_SHORT).show();
    // }

    if (newUri == null) {
        // Если null, значит ошибка при вставке.
        Toast.makeText(this, "Ошибка при заведении гостя", Toast.LENGTH_SHORT).show();
    } else {
        Toast.makeText(this, "Гость заведён успешно",
                Toast.LENGTH_SHORT).show();
    }
}

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

Перейдём к следующему этапу. Заменим TextView на ListView.

Создадим разметку res/layout/list_item.xml для отдельного элемента списка, состоящий из двух текстовых меток для имени и города.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="@dimen/activity_horizontal_margin">

    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif-medium"
        android:textAppearance="?android:textAppearanceMedium"
        android:textColor="#2B3D4D" />

    <TextView
        android:id="@+id/summary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="sans-serif"
        android:textAppearance="?android:textAppearanceSmall"
        android:textColor="#AEB6BD" />
</LinearLayout>

В макете content_main.xml заменим TextView на ListView. Я оставил на память, позже можно удалить TextView. Также добавлены компоненты на случай, если список будет пуст.


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/content_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="ru.alexanderklimov.cathouse.MainActivity"
    tools:showIn="@layout/activity_main">

    <TextView
        android:id="@+id/text_view_info"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="" />

    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <!-- Empty view for the list -->
    <RelativeLayout
        android:id="@+id/empty_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true">

        <ImageView
            android:id="@+id/empty_hotel_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            app:srcCompat="@drawable/ic_pets_black_24dp"/>

        <TextView
            android:id="@+id/empty_title_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/empty_hotel_image"
            android:layout_centerHorizontal="true"
            android:fontFamily="sans-serif-medium"
            android:paddingTop="16dp"
            android:text="Нет гостей"
            android:textAppearance="?android:textAppearanceMedium"/>

        <TextView
            android:id="@+id/empty_subtitle_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/empty_title_text"
            android:layout_centerHorizontal="true"
            android:fontFamily="sans-serif"
            android:paddingTop="8dp"
            android:text="Позовите кого-нибудь"
            android:textAppearance="?android:textAppearanceSmall"
            android:textColor="#A2AAB0"/>
    </RelativeLayout>
</RelativeLayout>

Вот так будет выглядеть сейчас экран активности.

Empty List

Добавим адаптер, который будет связывать данные из провайдера со списком.


package ru.alexanderklimov.cathouse;

import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;

/**
 * {@link GuestCursorAdapter} is an adapter for a list or grid view
 * that uses a {@link Cursor} of guest data as its data source. This adapter knows
 * how to create list items for each row of guest data in the {@link Cursor}.
 */
public class GuestCursorAdapter extends CursorAdapter {

    /**
     * Constructs a new {@link GuestCursorAdapter}.
     *
     * @param context The context
     * @param cursor       The cursor from which to get the data.
     */
    public GuestCursorAdapter(Context context, Cursor cursor) {
        super(context, cursor, 0);
    }

    /**
     * Makes a new blank list item view. No data is set (or bound) to the views yet.
     *
     * @param context app context
     * @param cursor  The cursor from which to get the data. The cursor is already
     *                moved to the correct position.
     * @param parent  The parent to which the new view is attached to
     * @return the newly created list item view.
     */
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
    }

    /**
     * This method binds the guest data (in the current row pointed to by cursor) to the given
     * list item layout. For example, the name for the current guest can be set on the name TextView
     * in the list item layout.
     *
     * @param view    Existing view, returned earlier by newView() method
     * @param context app context
     * @param cursor  The cursor from which to get the data. The cursor is already moved to the
     *                correct row.
     */
    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        TextView nameTextView = (TextView) view.findViewById(R.id.name);
        TextView summaryTextView = (TextView) view.findViewById(R.id.summary);

        // Find the columns of guest attributes that we're interested in
        int nameColumnIndex = cursor.getColumnIndex(GuestEntry.COLUMN_NAME);
        int cityColumnIndex = cursor.getColumnIndex(GuestEntry.COLUMN_CITY);

        // Read the guest attributes from the Cursor for the current guest
        String guestName = cursor.getString(nameColumnIndex);
        String guestCity = cursor.getString(cityColumnIndex);

        // If the city is empty string or null, then use some default text
        // that says "Unknown", so the TextView isn't blank.
        if (TextUtils.isEmpty(guestCity)) {
            guestCity = "unknown";
        }

        // Update the TextViews with the attributes for the current guest
        nameTextView.setText(guestName);
        summaryTextView.setText(guestCity);
    }
}

В MainActivity добавим ссылки на новые компоненты.


/** Адаптер для ListView */
GuestCursorAdapter mCursorAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ...

    //mDbHelper = new HotelDbHelper(this);

    ListView guestListView = (ListView) findViewById(R.id.list);

    // Если список пуст
    View emptyView = findViewById(R.id.empty_view);
    guestListView.setEmptyView(emptyView);

    // Адаптер
    // Пока данных нет используем null
    mCursorAdapter = new GuestCursorAdapter(this, null);
    guestListView.setAdapter(mCursorAdapter);
}

Извлечение данных является трудоёмким процессом, поэтому следует использовать отдельный поток для этой операции. Для работы с курсорами существует интерфейс LoaderManager.LoaderCallbacks<Cursor> с тремя методами. Реализуем его в активности.


public class MainActivity extends AppCompatActivity implements
        LoaderManager.LoaderCallbacks<Cursor> {
        
    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        // Зададим нужные колонки
        String[] projection = {
                GuestEntry._ID,
                GuestEntry.COLUMN_NAME,
                GuestEntry.COLUMN_CITY};

        // Загрузчик запускает запрос ContentProvider в фоновом потоке
        return new CursorLoader(this,
                GuestEntry.CONTENT_URI,   // URI контент-провайдера для запроса
                projection,             // колонки, которые попадут в результирующий курсор
                null,                   // без условия WHERE
                null,                   // без аргументов
                null);                  // сортировка по умолчанию
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        // Обновляем CursorAdapter новым курсором, которые содержит обновленные данные
        mCursorAdapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        // Освобождаем ресурсы
        mCursorAdapter.swapCursor(null);
    }
}       

Удалим метод displayDatabaseInfo() и метод активности onStart(). Теперь данные мы будем получать через загрузчик. Добавим код в onCreate().


private static final int GUEST_LOADER = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       
        ...

        getLoaderManager().initLoader(GUEST_LOADER, null, this);
    }

Также можно удалить и TextView. Все данные теперь будут попадать в список.

Переходим в GuestProvider и в методе query() добавляем строчку кода перед return:


cursor.setNotificationUri(getContext().getContentResolver(), uri);

Похожий код вставляем в метод insertGuest():


getContext().getContentResolver().notifyChange(uri, null);

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

Мы можем попасть на вторую активность через кнопку FAB для заведения нового гостя. Но у нас должна быть возможность отредактировать уже существующего гостя, который отображается в списке. Таким образом нам нужно добавить обработку нажатия на элементах списка.


guestListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Intent intent = new Intent(MainActivity.this, EditorActivity.class);

        Uri currentGuestUri = ContentUris.withAppendedId(GuestEntry.CONTENT_URI, id);
        intent.setData(currentGuestUri);
        startActivity(intent);
    }
});

В активности EditorActivity добавим код для приёма намерения.


private Uri mCurrentGuestUri;

private static final int EXISTING_GUEST_LOADER = 0;

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

    Intent intent = getIntent();
    mCurrentGuestUri = intent.getData();

    if (mCurrentGuestUri == null) {
        setTitle("Новый гость");

    } else {
        setTitle("Изменение данных");
    }

    ...
}

Теперь мы можем переходить по щелчку по элементу списка. При этом заголовок будет "Изменение данных", чтобы было понятно, что мы находимся в режиме редактирования данных о госте.

Когда мы хотим редактировать данные о госте, нам нужно подгрузить его данные. Подключаем к активности интерфейс LoaderCallbacks и его методы.


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

Инициализируем загрузчик в onCreate().


getLoaderManager().initLoader(EXISTING_GUEST_LOADER, null, this);

Заменим метод insertGuest() на расширенную версию метода saveGuest(), который будет вставлять или редактировать данные в зависимости от логики программы.

Также включим "защиту от дурака", если попытаемся завести пустые данные.

Осталось написать код для удаления гостя из базы данных.


case R.id.action_delete:
    showDeleteConfirmationDialog();
    return true;
    
private void showDeleteConfirmationDialog() {
    // Create an AlertDialog.Builder and set the message, and click listeners
    // for the postivie and negative buttons on the dialog.
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setMessage(R.string.delete_dialog_msg);
    builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            // User clicked the "Delete" button, so delete the guest.
            deleteGuest();
        }
    });
    builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            // User clicked the "Cancel" button, so dismiss the dialog
            // and continue editing the guest.
            if (dialog != null) {
                dialog.dismiss();
            }
        }
    });

    // Create and show the AlertDialog
    AlertDialog alertDialog = builder.create();
    alertDialog.show();
}

private void deleteGuest() {
    // Only perform the delete if this is an existing guest.
    if (mCurrentGuestUri != null) {
        // Call the ContentResolver to delete the guest at the given content URI.
        // Pass in null for the selection and selection args because the mCurrentGuestUri
        // content URI already identifies the guest that we want.
        int rowsDeleted = getContentResolver().delete(mCurrentGuestUri, null, null);

        // Show a toast message depending on whether or not the delete was successful.
        if (rowsDeleted == 0) {
            // If no rows were deleted, then there was an error with the delete.
            Toast.makeText(this, "Ошибка при удалении гостя",
                    Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "Гость успешно удален",
                    Toast.LENGTH_SHORT).show();
        }
    }

    // Закрываем активность
    finish();
}

На главной активность есть пункт меню "Удалить всех гостей".


case R.id.action_delete_all_entries:
    deleteAllGuest();
    return true;
    
private void deleteAllGuest() {
    int rowsDeleted = getContentResolver().delete(GuestEntry.CONTENT_URI, null, null);
}

Всё!

Код получился просто громадным. Если не заниматься контент-провайдерами постоянно, то вряд ли получится самостоятельно написать такую программу. Вот почему разработчики не любят контент-провайдер. Также вы можете пройти курс на Udacity - Android Basics: Data Storage | Udacity, в котором рассматривается контент-провайдер очень подробно (на англ.). В целях совместимости я даже переписал код, максимально похожий на код из курса.

Также вы можете скачать исходный код.

Реклама