Изначально в этой статья я хотел описать способ работы с SimpleCursorAdapter, который примичателен своей способностью биндить (привязывать, сопоставлять) колонки курсора с вьюшками, что весьма помогает при разработке и помогает избежать создания и хранения в памяти промежуточных объектов как, например, в подходе описанном мной тут.
Но в процесс написания я столкнулся с проблемой быстрого и эффективного заполнения базы данных набором исходных значений и описания ее схемы при помощи минимального количества кода. И для решения этой проблемы мы будем использовать SQL-скрипты — обычные текстовые файлы, содержание набор sql-запросов, как тот, что вы можете видеть ниже:
DROP TABLE IF EXISTS peoples;
CREATE TABLE peoples (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
photo TEXT NOT NULL DEFAULT 'default.png'
);
CREATE INDEX name_index ON peoples (name);
INSERT INTO peoples (name) VALUES ('name1');
INSERT INTO peoples (name, photo) VALUES ('name2', 'pict2.png');
Содержание
Для быстрой навигации по статье можно кликать по элементам списка.
- SqliteScript — класс, инкапсулирующий в себя скприпт и выполняющий его в нужной базе данных;
- Использование SimpleCursorAdapter — привязка данных вида «курсов -> элемент списка»
- Добавление специальной обработки при биндинге курсора к элементам списка при помощи ViewBinder
SqliteScript — как и зачем
Допустим, перед нами стоит задача заполнить базу данных значеними по умолчанию, делать это при помощи java-кода трудоемко, а мы ленивы и сообразительны. Поэтому для инициализации базы мы напишем вот такой скрипт:
DROP TABLE IF EXISTS peoples;
CREATE TABLE peoples (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
photo TEXT NOT NULL DEFAULT 'default.png
');
CREATE INDEX name_index ON peoples (name);
INSERT INTO peoples (name) VALUES ('X-man');
INSERT INTO peoples (name, photo) VALUES ('Dude', 'dude.png');
INSERT INTO peoples (name, photo) VALUES ('Turkish', 'turkish.png');
INSERT INTO peoples (name, photo) VALUES ('Vincent Vega', 'vincent.png');
Что он делает:
- Создает в базе таблицу и определяет ее схему;
- Создает индекс по нужному полю;
- Заполняет базу исходными данными;
Хотите проверить как он работает перед тем как использовать? Я пользуюсь плагином для Firefox SQLite Manager — удобная штука.
Первый этап выполнен. Теперь перед нами стоит другой вопрос: как выполнить этот скрипт на мобильном девайсе?
Все будет крайне просто, все что нам нужно — это тектовый файл скрипта. А лежать он может где угодно: в assets, интернете или на SD-карте. В нашем примере для простоты я положу его в папку /assets Android-проекта.
Теперь перед нами стоит другая задача — считать скприпт запрос за запросом и выполнить его. Решение можно увидеть ниже:
/**
* Класс, инкапсулирующий SQL-скрипт.
*
* @author dmitrykunin
*/
public class SqliteScript {
/*
* Тут будем построчно хранить запросы.
*/
private ArrayList<String> mStatements = new ArrayList<String>();
/**
* Инициализирует скрипт из строки.
*
* @param script строка, содержащая скрипт
*/
public SqliteScript(String script) {
Scanner scanner = new Scanner(script);
init(scanner);
}
/**
* Инициализирует скрипт из входного потока.
*
* @param input входной поток, содержащий скрипт
*/
public SqliteScript(InputStream input) {
Scanner scanner = new Scanner(input);
init(scanner);
}
/**
* Выполнение скрипта в одной транзакции,
* с целью отката изменений в случае ошибки.
*
* @param db
*/
public void runScript(SQLiteDatabase db) {
db.beginTransaction();
try {
for (String statement : mStatements) {
db.execSQL(statement);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
private void init(Scanner scanner) {
scanner.useDelimiter(";");
while (scanner.hasNext()) {
String statement = scanner.next().trim();
if (!TextUtils.isEmpty(statement))
mStatements.add(statement);
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("SqliteScript [mStatements(" + mStatements.size() + ")=");
builder.append(mStatements);
builder.append("]");
return builder.toString();
}
}
Если вы еще не знакомы с классом Scanner, то рекомендую познакомиться. Он очень полезен для задач, связанных с разбором строк.
Все, теперь мы можем выполнять скрипты. Приведенный класс можно усовершенствовать, добавив обработку комментариев в скрипте, возможноть удаления/добавки запросов и тд. Пример использования будет ниже.
Использование SimpleCursorAdapter
Класс SimpleCursorAdapter — наш друг. Если этого утверждения мало для того, чтобы вы его полюбили, то далее я объясню почему. Этот класс позволяет нам получать данные напрямую из курсора и «привязывать» их к нашему представлению (ListView
, GridView
etc.) парой строк, например так:
mCursor = database.query(TABLE_PEOPLES, null, null, null, null, null, null);
//Первый массив содержит имена колонок базы, второй - айдишники элементов списка.
//Вданном примере значение из колонки COLUMN_NAME будет присвоено вьюшке с айдишником R.id.name
final String[] from = {COLUMN_NAME, COLUMN_PHOTO};
final int[] to = {R.id.name, R.id.photo};
IDevScriptDBAdapter adapter = new IDevScriptDBAdapter(this, R.layout.list_item, mCursor, from, to);
setListAdapter(adapter);
Все текстовые значения этот сообразительный класс присвоит автоматически, даже попытается обработать изображения, но в данном случае он использует значение в БД как URI к картинке, что не всегда (никогда?) соответсвует действительности. Как делать кастомную обработку для подобных случаев вы узнаете в 3ей части статьи.
Для любопытных могу предложить самостоятельно изучить следующие методы, которые как раз и отвечают за механизм присвоения значения вьющкам: setViewText и setViewImage.
Для начал немного рутины, а именны опен-хелпер и описание констант для базы данных.
public class IDevSqliteOpenHelper extends SQLiteOpenHelper {
private static final int DB_VERSION = 2;
private static final String DB_NAME = "database.db";
private SqliteScript mScript;
public IDevSqliteOpenHelper(Context context, SqliteScript script) {
super(context,DB_NAME, null, DB_VERSION);
mScript = script;
}
@Override
public void onCreate(SQLiteDatabase db) {
mScript.runScript(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onCreate(db);
}
}
Об SQLiteOpenHelper я уже писал тут.
Теперь опишем константы — имена и индексы полей в базе данных. Тут я предлагаю вам перенять удобную практику использования интерфейсов для хранения констант, это позволит вам получить к ним доступ просто унаследовав этот интерфейс без всяких обязательств.
public interface DataBaseColumns {
public static String TABLE_PEOPLES = "peoples";
public static String COLUMN_NAME = "name";
public static int INDEX_NAME = 1;
public static String COLUMN_PHOTO = "photo";
public static int INDEX_PHOTO = 2;
}
С рутиной покочили, перейдем непосредственно к адаптеру:
public class IDevScriptDBAdapter extends SimpleCursorAdapter implements DataBaseColumns {
private Context mContext;
@SuppressWarnings("deprecation")
public IDevScriptDBAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
super(context, layout, c, from, to);
mContext = context;
}
}
Все просто!
Теперь будет больше кода в нашем активити:
public class SqliteScriptActivity extends ListActivity implements OnClickListener, DataBaseColumns {
private Button mButton;
private Cursor mCursor;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sqlite_script);
mButton = (Button) findViewById(R.id.btn);
mButton.setOnClickListener(this);
}
@Override
public void onClick(View v) {
tryReleaseCursor();
try {
SqliteScript script = new SqliteScript(getAssets().open("init.sql"));
IDevSqliteOpenHelper dbHelper = new IDevSqliteOpenHelper(this, script);
SQLiteDatabase database = dbHelper.getWritableDatabase();
mCursor = database.query(TABLE_PEOPLES, null, null, null, null, null, null);
final String[] from = {COLUMN_NAME, COLUMN_PHOTO};
final int[] to = {R.id.name, R.id.photo};
IDevScriptDBAdapter adapter = new IDevScriptDBAdapter(this, R.layout.list_item, mCursor, from, to);
startManagingCursor(mCursor);
setListAdapter(adapter);
} catch (IOException e) {
e.printStackTrace();
}
}
private void tryReleaseCursor() {
if (mCursor != null) {
stopManagingCursor(mCursor);
mCursor.close();
mCursor = null;
}
}
}
И разметка элемента списка:
<?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="100dp"
android:orientation="horizontal"
android:weightSum="4" >
<ImageView
android:id="@+id/photo"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="centerInside" />
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
android:layout_marginLeft="20dp"
android:gravity="center_vertical|left"
android:text="foo name" />
</LinearLayout>
Теперь можете запустить приложение. И увидеть, что картинок нет. Это потому, что наш адаптер не знает как правильно интерпритировать значения в базе, чтобы присвоить их в качестве изображения нашим ImageView
. Решение этой проблемы ниже.
ViewBinder
Интерфейс ViewBinder как раз и позволит нам добавить кастомную обработку в адаптер, для этого изменим код следующим образом:
public class IDevScriptDBAdapter extends SimpleCursorAdapter implements ViewBinder, DataBaseColumns {
private Context mContext;
@SuppressWarnings("deprecation")
public IDevScriptDBAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
super(context, layout, c, from, to);
mContext = context;
//Устанавливаем самого себя в качестве обработчика-ViewBinder'а
setViewBinder(this);
}
@Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
if (columnIndex == INDEX_PHOTO) {
try {
InputStream photoStream = mContext.getAssets().open(cursor.getString(columnIndex));
Bitmap photo = BitmapFactory.decodeStream(photoStream);
((ImageView)view).setImageBitmap(photo);
return true;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
return false;
}
}
Как видите, теперь наш адаптер имплементирует ViewBinder
и назначает в качестве обработчика самого себя, вытаскивая соотсветствующие значению в базе картинки из рескурсов /assets проекта. В конце метод setViewValue
возвращает true
, подтверждая успешность обработки.
Вот теперь все картинки должны быть на месте.
Удачного биндинга!