Изначально в этой статья я хотел описать способ работы с 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');

Содержание

Эти милые ребята покажутся нам на экране по завершении нашей работы над приложением.

Для быстрой навигации по статье можно кликать по элементам списка.

  1. SqliteScript — класс, инкапсулирующий в себя скприпт и выполняющий его в нужной базе данных;
  2. Использование SimpleCursorAdapter — привязка данных вида «курсов -> элемент списка»
  3. Добавление специальной обработки при биндинге курсора к элементам списка при помощи 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 — удобная штука.

Sqlite Manager для Firefox. И структура базы после выполнения скрипта.

Первый этап выполнен. Теперь перед нами стоит другой вопрос: как выполнить этот скрипт на мобильном девайсе?

Все будет крайне просто, все что нам нужно — это тектовый файл скрипта. А лежать он может где угодно: в assets, интернете или на SD-карте. В нашем примере для простоты я положу его в папку /assets Android-проекта.

Положили файл с sql-скриптом в /assets проекта.

Теперь перед нами стоит другая задача — считать скприпт запрос за запросом и выполнить его. Решение можно увидеть ниже:


/**
* Класс, инкапсулирующий 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, подтверждая успешность обработки.

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

Удачного биндинга! :)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *