Сложно представить серьезное приложение, которое не нуждается в хранении данных. В Android существует несколько способов для этого: SharedPreferences, использование файлов, использование веб-сервисов и Sqlite базы данных. Сегодня мы рассмотрим последний вариант в достаточном объеме.
Суть проблемы
При создании приложения с базой данных можно разделить работу на 3 этапа:
- Описание моделей и проектирование базы данных на их основе
- Описание классов для управления данными в базе
- Привязка данных к представлению (пользовательскому интерфейсу)
Мы пройдем все три чтобы создать наше приложение, которое назовем IdevBabysName.
Шаг 1: описание моделей. Или что мы вообще будем делать?
В качестве примера создадим приложение BabysName, которое поможет выбрать имя ребенку из списка вариантов.
Функционал: добавить имя, удалить имя, выбрать случайное имя из списка.
Скриншот все расскажет:
Тут нам достаточно одной модели — имени. Опишем ее к классе Name:
package by.idev.android.babyssql;
public class Name {
private long id;
private String name;
public Name(String name) {
this.name = name;
}
public Name(long id, String name) {
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
}
В ней достаточно двух полей:собственно имя в виде строки и id в виде long. Исходя из этого наша база будет иметь два поля: целочисленное для id, и текстовое name.
Внимание! Всегда задавайте для поля id имя «_id» в вашей базе! Это соглашение, и если вы не будите следовать ему ваша база не сможет быть использована со многими стандартными классами и Content Provider‘ми.
Шаг 2: SQLiteOpenHelper — создание, открытие и управление версиями БД
На самом деле существует два метода открытия базы: напрямую методом openOrCreateDatabase() контекста либо использовать класс SQLiteOpenHelper для более тонкой работы.
Рассмотрим первый вариант, код будет иметь следующий вид:
private static final String DB_NAME = "somebase.sqlite3";
private static final String TABLE_NAME = "sometable";
//Подготовленный запрос для создания таблицы в базе
private static final String DB_CREATE_QUERY =
"CREATE TABLE " + TABLE_NAME +
"(_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"column_one TEXT NOT NULL);";
SQLiteDataBase someDb;
private void openDataBase() {
//Создаем базу в памяти устройства
someDb = openOrCreateDataBase(DB_NAME, Context.MODE_PRIVATE, null);
//Используем sql-запрос для создания таблицы
someDb.execSQL(DB_CREATE_QUERY);
}
Этот код можно поместить в ваш Activity или Application, и работать с базой someDb. Это быстрый метод подходящий для простых «одноразовых» приложений, в более серьезном проекте желательно использовать второй подход, вы очень быстро поймете почему.
Приведем описание нашего openhelper‘а:
// Класс-помошник отвечающий за создание/отктрытие
// базы и осуществляющий контроль ее версий
private static class DbOpenHelper extends SQLiteOpenHelper {
public DbOpenHelper(Context context, String name,
CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
// Вызывается при создании базы на устройстве
public void onCreate(SQLiteDatabase db) {
// Посроим стандартный sql-запрос для создания таблицы
final String CREATE_DB = "CREATE TABLE " + TABLE_NAME + " ("
+ KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ KEY_NAME + " TEXT NOT NULL);";
db.execSQL(CREATE_DB);
}
@Override
// Метод будет вызван, если изменится версия базы
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Тут можно организовать миграцию данных из старой базы в новую
// или просто "выбросить" таблицу и создать заново
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
Перегрузка двух указанных методов обязательна, как и описание конструктора. Смысл методов примерно таков:
- onCreate() — запускается если база еще не создана, то есть обычно при первом запуске приложения. В нем опишем код чегобывыдумали? Да, создания базы.
- onUpgrade() — будет вызван, если изменилась версия базы данных. Допустим, если в нее добавили новый столбец, тогда код в этом методе будет иметь вид:
db.execSQL(ALTER TABLE table_name ADD column_name datatype);
В нашем случае мы просто заменим таблицу на новую, чтобы не вносить сложности в пример.
Вы наверняка заметили, что DbOpenHelper обявлен как private static. Все потому, что он будет статическим вложенным классом у класса SqlAdapter, который свяжет контролер (Activity) и базу данных. Сама Activity не будет знать о базе данных, а лишь обращаться к методам адаптера. Таким образом мы сможем менять наш источник данных не меняя код контролера. И это хорошо!
Шаг 2: описания адаптера и операций с базой данных
Наш адаптер будет унаследован от класса BaseAdapter, что позволит нам использовать его с ListView. Для начала представлю его код полностью, а потом рассмотрим интересные нам методы в индивидуальном порядке:
package by.idev.android.babyssql;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.CursorIndexOutOfBoundsException;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
public class SqlAdapter extends BaseAdapter {
private static final String DB_NAME = "babys_names.sqlite3";
private static final String TABLE_NAME = "names";
private static final int DB_VESION = 1;
// Для удобства выполнения sql-запросов
// создадим константы с именами полей таблицы
// и номерами соответсвующих столбцов
private static final String KEY_ID = "_id";
private static final int ID_COLUMN = 0;
private static final String KEY_NAME = "name";
private static final int NAME_COLUMN = 1;
private Cursor cursor;
private SQLiteDatabase database;
private DbOpenHelper dbOpenHelper;
private Context context;
//Далее следуют обязательные к перегрузке методы адаптера
public SqlAdapter(Context context) {
super();
this.context = context;
init();
}
@Override
public long getItemId(int position) {
Name nameOnPosition = getItem(position);
return nameOnPosition.getId();
}
@Override
public View getView(int position, View convertView, ViewGroup parrent) {
TextView textView;
if (null == convertView) {
textView = (TextView) View.inflate(context, R.layout.list_item,
null);
} else {
textView = (TextView) convertView;
}
textView.setText(getItem(position).getName());
return textView;
}
@Override
public int getCount() {
return cursor.getCount();
}
@Override
public Name getItem(int position) {
if (cursor.moveToPosition(position)) {
long id = cursor.getLong(ID_COLUMN);
String name = cursor.getString(NAME_COLUMN);
Name nameOnPositon = new Name(id, name);
return nameOnPositon;
} else {
throw new CursorIndexOutOfBoundsException(
"Cant move cursor to postion");
}
}
//Методы для работы с базой данных
public Cursor getAllEntries() {
//Список колонок базы, которые следует включить в результат
String[] columnsToTake = { KEY_ID, KEY_NAME };
// составляем запрос к базе
return database.query(TABLE_NAME, columnsToTake,
null, null, null, null, KEY_ID);
}
public long addItem(Name name) {
ContentValues values = new ContentValues();
values.put(KEY_NAME, name.getName());
long id = database.insert(TABLE_NAME, null, values);
refresh();
return id;
}
public boolean removeItem(Name nameToRemove) {
boolean isDeleted = (database.delete(TABLE_NAME, KEY_NAME + "=?",
new String[] { nameToRemove.getName() })) > 0;
refresh();
return isDeleted;
}
public boolean updateItem(long id, String newName) {
ContentValues values = new ContentValues();
values.put(KEY_NAME, newName);
boolean isUpdated = (database.update(TABLE_NAME, values, KEY_ID + "=?",
new String[] {id+""})) > 0;
return isUpdated;
}
//Прочие служебные методывфзеук
public void onDestroy() {
dbOpenHelper.close();
}
//Вызывает обновление вида
private void refresh() {
cursor = getAllEntries();
notifyDataSetChanged();
}
// Инициализация адаптера: открываем базу и создаем курсор
private void init() {
dbOpenHelper = new DbOpenHelper(context, DB_NAME, null, DB_VESION);
try {
database = dbOpenHelper.getWritableDatabase();
} catch (SQLException e) {
// /Если база не открылась, то дальше нам дороги нет
// но это особый случай
Log.e(this.toString(), "Error while getting database");
throw new Error("The end");
}
cursor = getAllEntries();
}
// Класс-помошник отвечающий за создание/отктрытие
// базы и осуществляющий контроль ее версий
private static class DbOpenHelper extends SQLiteOpenHelper {
//код был представлени ранее
}
}
«И о чем эта песня?» С константами все должно быть понятно, они нужны для составления запросов. Первая группа методов позволяет нашему адаптеру общаться с ListView, мы не будем останавливаться на этом, так как это не тема статьи. Если вы никогда не писали своего кастомного адаптера, то просто верьте на слово =)
Нас гораздо больше интересует группа методов для работы с базой данных. И в самом первом из них мы встречаемся с классом Cursor. Объект этого класса возвращается нам после запроса в базу данных. Он очень напоминает итератор своими методами moveToFirst(), moveToNext() etc. Получить курсор можно следующим образом:
public Cursor getAllEntries() {
//Список колонок базы, которые следует включить в результат
String[] columnsToTake = { KEY_ID, KEY_NAME };
// составляем запрос к базе
return database.query(TABLE_NAME, columnsToTake,
null, null, null, null, KEY_ID);
}
Метод query() класса SQLiteDatabase в простейшем случае принимает имя таблицы, массив строк с именами столбцов которые нужно поместить в результат и последнем значением имя столбца, по которому результат будет отсортирован.
Один из перегруженных методов адаптера является по совместительству и методом использующим курсор, то есть тоже «наш клиент»:
@Override
public Name getItem(int position) {
if (cursor.moveToPosition(position)) {
long id = cursor.getLong(ID_COLUMN);
String name = cursor.getString(NAME_COLUMN);
Name nameOnPositon = new Name(id, name);
return nameOnPositon;
} else {
throw new CursorIndexOutOfBoundsException(
"Cant move cursor to postion");
}
}
Тут мы видим пример извлечения данных из курсора. Замечу, что данные в sqlite слабо типизированные, т.е. вы можете прочитать дробное число в строку и все в этом духе. Если бы вам нужно было извлечь все записи, то это можно было бы сделать так:
ArrayList names = new ArrayList();
cursor.moveToFirst();
if (!cursor.isAfterLast()) {
do {
long id = cursor.getLong(ID_COLUMN);
String name = cursor.getString(NAME_COLUMN);
names.add(new Name(id, name));
} while (cursor.moveToNext());
}
После этого массив names будет содержать все имена из базы данных.
Рассмотрим добавление элемента в базу:
public long addItem(Name name) {
ContentValues values = new ContentValues();
values.put(KEY_NAME, name.getName());
long id = database.insert(TABLE_NAME, null, values);
refresh();
return id;
}
Тут мы сталкиваемся к классом ContentValue, объекты которого содержат множество пар типа «ключ-значение», где ключ — имя колонки в БД, а значение… это ее значение! Метод insert() достаточно прост, вы уже сами заметили. Но что это за метод refresh()? Он из области служебных методов:
//Вызывает обновление вида
private void refresh() {
cursor = getAllEntries();
notifyDataSetChanged();
}
Его роль в том, чтобы обеспечить нас «свежим» курсором с последними данными и оповестить вид об изменениях в данных. Мы будем обращаться к этому методу каждый раз после внесения изменений в базу.
Методы removeItem() и updateItem() очень похожи, рассмотрим первый:
public boolean removeItem(Name nameToRemove) {
boolean isDeleted = (database.delete(TABLE_NAME, KEY_NAME + "=?",
new String[] { nameToRemove.getName() })) > 0;
refresh();
return isDeleted;
}
Второй аргумент метода delete() это ничто иное как WHERE из SQL, третий же — это его аргументы, на которые будут заменены знаки «?» (whildcard) в описании WHERE. Таким образом наше выражение примет вид:
DELETE FROM TABLE table_name WHERE key_name = nameToRemove
продолжение тут, а исходники тут IdevBabysSQL
Вакантное место: ведущий Android разработчик в компании Softeq Development
Что будете делать Вы?
- разрабатывать приложения для мобильных Android устройств
- работать с заказчиками мирового уровня
Что нужно от Вас?
- опыт разработки под Android от одного года
- отличное знание основ Java
- отличное знание основ Android
- хорошее знание С/С++ и опыт работы с NDK
- знание принципов ООП и способность эффективно применять их в архитектуре приложения
- знание и умение применять на практике шаблоны проектирования
- английский язык: уметь уверенно читать и писать, разговорный приветствуется
Большим плюсом будет:
- опыт кросс-платформенного программирования
Сайт: www.softeq.by, www.softeq.com
Cтраничка в Facebook: http://www.facebook.com/pages/Softeq/110374298801
Страничка в VK: http://vk.com/club21079655
Контактная информация:
Ждем Ваше резюме на jobs@softeq.by!
Если вы хотите узнать о вакансии и нашей компании больше, Вы можете связаться с менеджером по персоналу Ириной Протащик:
Skype: irina.protaschik
E-mail: irina.protaschik@softeq.com
Моб.: +375 29 175 47 00, +375 29 234 47 00