Приложение TutList, с которым мы работали, сейчас имеет довольно большой недостаток: данные статьи не «живые», а статические. В этом руководстве вы предпримете еще несколько шагов к гибкому и расширяемому решению, изменив приложение, чтобы оно выступало в качестве поставщика контента с поддержкой данных.
Платформа Android использует концепцию, называемую поставщиками контента, которая позволяет приложениям обмениваться данными и использовать их на платформе. Обычно поставщик поддерживается базой данных SQLite, в которой хранятся базовые данные. Текущее состояние приложения TutList таково, что оно получает данные для ListView из статических строковых массивов в ресурсах. В этом руководстве мы удалим эти фиксированные ресурсы данных и создадим на их месте гибкого поставщика контента на основе базы данных. Вы увидите, что код пользовательского интерфейса не сильно изменится. Серверная часть, однако, станет более сложной. Преимущество здесь в том, что когда мы наконец переключим приложение на извлечение оперативных данных из Интернета, у нас будет место для удобного хранения и управления ими.
Темп этого урока будет быстрее, чем у некоторых наших уроков для начинающих; вам, возможно, придется просмотреть некоторые другие учебники по Android на этом сайте или даже в справочнике по Android SDK, если вы не знакомы с какими-либо базовыми концепциями и классами Android, обсуждаемыми в этом учебном пособии. Вы можете прочитать учебное пособие по ускоренному курсу SQLite для разработчиков Android, чтобы освежить свои знания SQLite. Окончательный пример кода, прилагаемый к этому учебному пособию, доступен для загрузки с открытым исходным кодом с хостинга кодов Google .
Шаг 0: Начало работы
В этом руководстве предполагается, что вы начнете с того места, где остановился наш последний учебник «Совместимость с Android: работа с фрагментами» . Вы можете скачать этот код и собрать его оттуда, или вы можете скачать код для этого урока и следовать за ним. В любом случае, будьте готовы, загрузив один или другой проект и импортировав его в Eclipse.
Шаг 1: Создание класса базы данных
Во-первых, вы должны создать базовую базу данных SQLite приложения. Начните с создания нового класса с именем TutListDatabase, который расширяется из SQLiteOpenHelper. Мы поместили его в пакет com.mamlambo.tutorial.tutlist.data, чтобы отделить его от части пользовательского интерфейса приложения. Пока вы это делаете, определите информацию о конфигурации базы данных в классе, такую как имя базы данных и номер ее версии. Мы сделали это с константами. Наконец, используйте обновление конструктора класса TutListDatabase для ссылки на эти значения следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
public class TutListDatabase extends SQLiteOpenHelper {
private static final String DEBUG_TAG = «TutListDatabase»;
private static final int DB_VERSION = 1;
private static final String DB_NAME = «tutorial_data»;
public TutListDatabase(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
|
Мы вскоре перейдем к реализации методов onCreate () и onUpgrade ().
Шаг 2: Определение схемы базы данных
Наши исходные данные статьи имели только два поля данных: заголовок и ссылку. Нет причин не сохранять такую структуру в нашей базе данных SQLite. Нам также потребуется обязательное поле _id, которое действует как уникальный идентификатор для каждой записи. Мы назовем эту таблицу учебниками.
Эта простая схема нам пока подойдет. Чтобы упростить использование и помочь с другими аспектами системы, мы определим столбцы, таблицы и даже оператор create как статические строки в классе TutListDatabase, например, так:
01
02
03
04
05
06
07
08
09
10
|
public static final String TABLE_TUTORIALS = «tutorials»;
public static final String ID = «_id»;
public static final String COL_TITLE = «title»;
public static final String COL_URL = «url»;
private static final String CREATE_TABLE_TUTORIALS = «create table » + TABLE_TUTORIALS
+ » (» + ID + » integer primary key autoincrement, » + COL_TITLE
+ » text not null, » + COL_URL + » text not null);»;
private static final String DB_SCHEMA = CREATE_TABLE_TUTORIALS;
|
Шаг 3: Создание базы данных
Создание базы данных теперь должно быть достаточно простым. В методе onCreate () просто выполните строку DB_SCHEMA как SQL, чтобы создать таблицу, определенную на предыдущем шаге:
1
2
3
4
|
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DB_SCHEMA);
}
|
Это может быть хорошей возможностью вставить некоторые образцы данных (статей) для использования. Вы можете использовать типичную команду «INSERT INTO» SQLite. Посмотрите код загрузки для примера того, как это сделать.
Шаг 4: Обновление базы данных
Поскольку база данных совершенно новая, у нас нет политики обновления, которой мы должны следовать. Поэтому мы просто удалим таблицу и создадим ее заново, если будет сделан запрос на обновление.
1
2
3
4
5
6
7
|
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(DEBUG_TAG, «Upgrading database. Existing contents will be lost. [«
+ oldVersion + «]->[» + newVersion + «]»);
db.execSQL(«DROP TABLE IF EXISTS » + TABLE_TUTORIALS);
onCreate(db);
}
|
Если версия базы данных увеличивается и приложение устанавливается поверх существующей установки, этот код будет запущен. Предупреждение, отображаемое только для LogCat, просто утверждает, что существующее содержимое будет потеряно. Если вы реализовали создание образца контента как часть предыдущего шага, исходный контент будет восстановлен во время создания. В опубликованном приложении, в котором хранятся важные пользовательские данные, вы, вероятно, захотите сделать все возможное, чтобы сохранить эти данные, перенеся их из старой схемы в новую. Однако для этого простого примера такие положения практически не нужны.
Поскольку схема базы данных не изменилась и остается совместимой, нет причин обновлять версию базы данных. Содержимое базы данных останется нетронутым.
Шаг 5: Создание класса провайдера контента
Теперь, когда у вашего приложения есть функциональная база данных, вы можете обратить внимание на реализацию поставщика контента для доступа, предоставления и управления данными статей, хранящимися там. Начните с создания нового класса с именем TutlistProvider, который расширяет класс ContentProvider. Дайте ему закрытую переменную-член для хранения экземпляра TutListDatabase. Создайте базу данных в методе onCreate ():
1
2
3
4
5
6
7
8
|
public class TutListProvider extends ContentProvider {
private TutListDatabase mDB;
@Override
public boolean onCreate() {
mDB = new TutListDatabase(getContext());
return true;
}
|
Шаг 6: Подготовка вспомогательных констант и соответствия
Контент-провайдеры работают с данными на уровне URI. Например, этот URI идентифицирует все учебники:
1
|
content:// com.mamlambo.tutorial.tutlist.data.TutListProvider/tutorials
|
Тем не менее, эта идентификация на самом деле не происходит по волшебству. Вместо этого классы поставщика контента обычно предоставляют некоторые открытые константы, которые могут использоваться приложениями для идентификации данных, которые они хотят запросить. Внутри контент-провайдера доступна некоторая справка для определения, какой тип URI передается. Это станет более понятным в следующих примерах и коде.
А пока вот набор констант, найденных в классе TutListProvider для использования в различных методах и для использования внешними классами, использующими этот поставщик контента:
01
02
03
04
05
06
07
08
09
10
11
12
|
private static final String AUTHORITY = «com.mamlambo.tutorial.tutlist.data.TutListProvider»;
public static final int TUTORIALS = 100;
public static final int TUTORIAL_ID = 110;
private static final String TUTORIALS_BASE_PATH = «tutorials»;
public static final Uri CONTENT_URI = Uri.parse(«content://» + AUTHORITY
+ «/» + TUTORIALS_BASE_PATH);
public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE
+ «/mt-tutorial»;
public static final String CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE
+ «/mt-tutorial»;
|
Обратите внимание, какие определения являются частными, а какие общедоступными — это целесообразно. Публичные определения будут использоваться другими частями приложения (или другими приложениями, которые хотят получить доступ к данным поставщика контента). Частные определения предназначены только для внутреннего использования классом и не доступны другим. Чтобы определить, какие типы адресов URI передаются поставщику содержимого, мы можем использовать вспомогательный класс UriMatcher для определения конкретных шаблонов URI, которые будет поддерживать поставщик содержимого. Вот UriMatcher для нашего провайдера контента, статически определенный на уровне класса внутри TutListProvider:
1
2
3
4
5
6
|
private static final UriMatcher sURIMatcher = new UriMatcher(
UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(AUTHORITY, TUTORIALS_BASE_PATH, TUTORIALS);
sURIMatcher.addURI(AUTHORITY, TUTORIALS_BASE_PATH + «/#», TUTORIAL_ID);
}
|
Этот UriMatcher определяет два типа URI. Один выглядит как образец сверху. К другому просто добавляется косая черта (/), за которой следует число. Этот тип URI используется для предоставления уникального идентификатора для конкретной статьи, чтобы возвратить одну запись.
Шаг 6. Обработка запросов провайдера контента
У провайдера контента есть несколько методов, которые мы должны переопределить. В настоящее время нас интересует только метод query (). Другими являются delete (), getType (), insert () и update () — мы вернемся к ним ближе к концу этого урока.
Метод query () изначально выглядит сложным, с пятью параметрами, включая два массива. Как выясняется, в Android SDK есть еще один вспомогательный класс, который значительно упрощает реализацию этого метода. Класс SQLiteQueryBuilder — это то, что мы ищем. Вот полная и простая реализация метода query ():
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TutListDatabase.TABLE_TUTORIALS);
int uriType = sURIMatcher.match(uri);
switch (uriType) {
case TUTORIAL_ID:
queryBuilder.appendWhere(TutListDatabase.ID + «=»
+ uri.getLastPathSegment());
break;
case TUTORIALS:
// no filter
break;
default:
throw new IllegalArgumentException(«Unknown URI»);
}
Cursor cursor = queryBuilder.query(mDB.getReadableDatabase(),
projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
|
Для начала мы получаем новый экземпляр класса SQLiteQueryBuilder. Затем мы используем метод setTables () для указания таблиц, с которыми мы работаем — в данном случае, просто таблицы учебных пособий. Далее мы используем класс UriMatcher для тяжелой работы по определению того, предназначен ли запрос для одной записи или для всех записей. Если это одна запись, мы добавляем предложение where для фильтрации по уникальному идентификатору.
Далее мы вызываем метод query () класса SQLiteQueryBuilder. Оказывается, что он принимает многие из тех же параметров, которые были переданы нашему методу query () — поэтому мы просто передаем их. Затем мы возвращаем вновь созданный курсор.
Метод setNotificationUri () просто помещает наблюдение в распознаватель содержимого вызывающей стороны , так что, если данные изменяются и у вызывающей стороны есть зарегистрированный наблюдатель изменений, они будут уведомлены. Здесь мы просто используем тот же URI.
Шаг 7: Регистрация провайдера контента
Как и класс Activity, поставщик контента должен быть надлежащим образом зарегистрирован в файле манифеста Android. Это означает добавление раздела в раздел файла, как показано ниже:
<поставщик Android: власти = "com.mamlambo.tutorial.tutlist.data.TutListProvider" андроид: многопроцессном = «истина» андроид: имя = "com.mamlambo.tutorial.tutlist.data.TutListProvider"> </ поставщик>
Атрибут полномочия должен соответствовать константе AUTHORITY, определенной в классе TutListProvider, поскольку это полномочия, используемые с URI. Атрибут name должен быть полностью определенным именем класса поставщика контента.
Шаг 8: Обновление ListView
Завершив реализацию провайдера контента, давайте обновим приложение, чтобы использовать его! В классе TutListFragment обновите метод onCreate (), чтобы использовать нового поставщика контента следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String[] projection = { TutListDatabase.ID, TutListDatabase.COL_TITLE };
String[] uiBindFrom = { TutListDatabase.COL_TITLE };
int[] uiBindTo = { R.id.title };
Cursor tutorials = getActivity().managedQuery(
TutListProvider.CONTENT_URI, projection, null, null, null);
CursorAdapter adapter = new SimpleCursorAdapter(getActivity()
.getApplicationContext(), R.layout.list_item, tutorials,
uiBindFrom, uiBindTo);
setListAdapter(adapter);
}
|
Проекция — это просто список столбцов для использования с адаптером. ListView использует заголовки и может предоставить идентификатор при нажатии элемента. Для использования с адаптером столбец id должен иметь имя «_id», что мы и сделали.
Затем вам нужно обновить метод onListItemClick () ListView. Раньше мы просто использовали позицию для поиска ссылки в массиве. Теперь мы будем использовать уникальный идентификатор, который достаточно удобно совпадает с идентификатором базы данных, и найдем ссылку в базе данных с помощью простого запроса к поставщику контента:
01
02
03
04
05
06
07
08
09
10
11
12
|
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
String projection[] = { TutListDatabase.COL_URL };
Cursor tutorialCursor = getActivity().getContentResolver().query(
Uri.withAppendedPath(TutListProvider.CONTENT_URI,
String.valueOf(id)), projection, null, null, null);
if (tutorialCursor.moveToFirst()) {
String tutorialUrl = tutorialCursor.getString(0);
tutSelectedListener.onTutSelected(tutorialUrl);
}
tutorialCursor.close();
}
|
Здесь мы запрашиваем столбец URL и используем URI содержимого с добавленным к нему идентификатором. Довольно просто, правда?
И угадайте, что? Вы сделали! Вы можете запустить приложение сейчас, и оно должно выглядеть и вести себя точно так же, как и раньше. Однако вместо того, чтобы получать данные из фиксированных ресурсов, приложение теперь хранит свои данные в новом, улучшенном доме — базе данных, и доступ к ним осуществляется с помощью простого механизма — поставщика контента.
Все это, и мы вернулись туда, откуда начали. Чувствую себя немного антиклиматично, да? На самом деле, вы не совсем закончили. Вы должны завершить остальные методы контент-провайдера.
Шаг 9: Завершение провайдера контента
Хотя приложение еще не использует методы insert (), update (), getType () или delete (), это произойдет в будущем, когда вы начнете собирать «живые» учебные данные из удаленного источника. Реализация каждого из этих методов следует шаблону, аналогичному методу query (). Например, вот реализация метода delete ():
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int uriType = sURIMatcher.match(uri);
SQLiteDatabase sqlDB = mDB.getWritableDatabase();
int rowsAffected = 0;
switch (uriType) {
case TUTORIALS:
rowsAffected = sqlDB.delete(TutListDatabase.TABLE_TUTORIALS,
selection, selectionArgs);
break;
case TUTORIAL_ID:
String id = uri.getLastPathSegment();
if (TextUtils.isEmpty(selection)) {
rowsAffected = sqlDB.delete(TutListDatabase.TABLE_TUTORIALS,
TutListDatabase.ID + «=» + id, null);
} else {
rowsAffected = sqlDB.delete(TutListDatabase.TABLE_TUTORIALS,
selection + » and » + TutListDatabase.ID + «=» + id,
selectionArgs);
}
break;
default:
throw new IllegalArgumentException(«Unknown or Invalid URI » + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsAffected;
}
|
Первые несколько строк определяют тип входящего URI и открывают доступную для записи базу данных. Затем, если URI указывает на список, возможно, с фильтром, мы просто удаляем это. Обратите внимание, что URI без фильтра (selection и selectionArgs) удалит все записи. В противном случае мы удаляем на основе определенного идентификатора — с или без фильтра.
Остальные методы находятся в загружаемом (и доступном для просмотра онлайн) открытом исходном коде. Они похожи, и вы должны быть в состоянии прочитать их, чтобы увидеть, как они работают. Суть в том, что каждый из них делает «правильную» вещь в зависимости от типа URI. Так как этот поставщик контента поддерживается базой данных, то обычно нужно вызывать эквивалентный метод SQLite, что значительно упрощает реализацию интерфейса.
Вывод
Из этого руководства вы узнали не только о том, как создать базу данных SQLite и обернуть ее внутри провайдера контента, но и о том, как просто использовать провайдер контента для заполнения элемента управления ListView. В будущих уроках мы расширим это приложение, чтобы наполнить базу данных приложения свежим, живым уроком и многим другим.
Мы надеемся, что вам понравился этот урок. Мы с нетерпением ждем ваших отзывов о темпах и сложности материала.
Об авторах
Разработчики мобильных приложений Лорен Дарси и Шейн Кондер являются соавторами нескольких книг по разработке Android: углубленная книга по программированию под названием « Разработка беспроводных приложений для Android» и « Самс научи себя разрабатывать приложения для Android за 24 часа» . Когда они не пишут, они тратят свое время на разработку мобильного программного обеспечения в своей компании и оказание консультационных услуг. С ними можно связаться по электронной почте [email protected] , через их блог на androidbook.blogspot.com и в Twitter @androidwireless .