Go to content
Your browser is out of date. It has known security flaws and may not display all features of this websites. Learn how to update your browser[Закрыть]

Старые поделки


Разработка под Android - Часть 4: Расширенные возможности программирования БД/GUI и DDMS


Автор: Серый череп



В этом руководстве будут освещены расширенные приемы программирования баз данных, а также некоторые приемы по разработке графического пользовательского интерфейса (GUI). В предыдущем посте мы научились вставлять данные в БД и извлекать их оттуда, а также создавать таблицы. Сейчас необходимо освоить, как удалять ненужные данные из базы и обновлять их, если ввод данных был произведен некорректно.  Мы свяжем эти возможности с сенсорным управлением - удаление или правка данных будет производиться длинным нажатием на экран и выделением фрагмента текста для редактирования.

Проект, в котором нам предстоит разобраться, взят из сайта Android-разработчиков под названием Notepad v2, небольшие изменения понадобились, чтобы адаптировать программу под наш предыдущий проект RandomQuotes. Мы используем уже готовый пример, меняя некоторые части кода. Сам код представляет собой отличную основу для построения GUI и баз данных, интересен как новичкам, так и более опытным разработчикам. Новый проект будет называться EnhancedQuotes, записи БД в нем будут отображаться в режиме ListView. Мы создадим новый проект с нуля вместо копирования старого. Ниже приведены данные для создания нового проекта:

  • Project Name: EnhancedQuotes
  • Build Target: Android 1.5
  • Application Name: EnhancedQuotes
  • Package Name: com.gregjacobs.enhancedquotes
  • Create Activity: QuotesMainMin
  • SDK Version: 3

После создания проекта приступаем к усовершенствованию нашего GUI, дополнению программы операторами удаления и обновления. Здесь нам нужно будет разделить код программы на отдельные файлы соответственно с функционалом программы. Это важный момент в современном программировании, так как он позволяет упорядочивать длинный код программы, выполнять функции для разных экранов и макетов более эффективно и рационально. Для данного проекта код будет разделен на три .java файла, соответственно, будет три разных файла макета. Начнем с основы - создания нового файла класса в пакете com.gregjacobs.enhancedquotes под названием QuotesDBAdapter. Он будет содержать код базы данных, мы создадим совершенно новый файл вместо копирования файла базы данных из предыдущего проекта.

package com.gregjacobs.enhancedquotes;
 
import java.util.Random;
 
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
 
public class QuotesDBAdapter {
 
	static Random random = new Random();
    public static final String KEY_QUOTES = "quotes";
    public static final String KEY_ROWID = "_id";
 
    private static final String TAG = "QuotesDbAdapter";
    private DatabaseHelper mDbHelper;
    private SQLiteDatabase mDb;
 
    /**
     * SQL запрос для создания БД
     */
    private static final String DATABASE_CREATE =
            "create table tblRandomQuotes (_id integer primary key autoincrement, "
                    + "quotes text not null);";
 
    private static final String DATABASE_NAME = "Random";
    private static final String DATABASE_TABLE = "tblRandomQuotes";
    private static final int DATABASE_VERSION = 2;
 
    private final Context mCtx;
 
    private static class DatabaseHelper extends SQLiteOpenHelper {
 
        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }
 
        @Override
        public void onCreate(SQLiteDatabase db) {
 
            db.execSQL(DATABASE_CREATE);
        }
 
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
                    + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS tblRandomQuotes");
            onCreate(db);
        }
    }
 
    /**
     * Constructor - по данным из контекста позволяет
     * открывать/создавать БД
     *
     * @param ctx - контекст, с которым работает программа
     */
    public QuotesDBAdapter(Context ctx) {
        this.mCtx = ctx;
    }
 
    public QuotesDBAdapter open() throws SQLException {
        mDbHelper = new DatabaseHelper(mCtx);
        mDb = mDbHelper.getWritableDatabase();
        return this;
    }
 
    public void close() {
        mDbHelper.close();
    }

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

    public long createQuote(String quotes) {
        ContentValues initialValues = new ContentValues();
        initialValues.put(KEY_QUOTES, quotes);
 
        return mDb.insert(DATABASE_TABLE, null, initialValues);
    }

В описании оператора insert первая переменная представляет собой таблицу базы данных, в которую производится вставка. Следующая переменная служит для ввода нулевых значений, если такие будут, и третья представляет собой значения, которые вставляются в таблицу.

    public boolean deleteQuote(long rowId) {
 
        return mDb.delete(DATABASE_TABLE, KEY_ROWID + "=" + rowId, null) > 0;
    }

Оператор delete содержит три значения в своем описании. Первая переменная - таблица базы данных, вторая представляет собой событие where. В нашем случае она нужна, но ее использование не всегда обязательно. Последняя переменная представляет собой аргументы события where, и если вы включите ее в предыдущую часть, все также будет работать. Стоит отметить, что можно также поставить “?” в оператор where и прописать их в третьей переменной.

    public Cursor fetchAllQuotes() {
 
        return mDb.query(DATABASE_TABLE, new String[] {KEY_ROWID, KEY_QUOTES}, null, null, null, null, null);
    }
 
    public Cursor fetchQuote(long rowId) throws SQLException {
 
        Cursor mCursor =
 
                mDb.query(true, DATABASE_TABLE, new String[] {KEY_ROWID,
                        KEY_QUOTES}, KEY_ROWID + "=" + rowId, null,
                        null, null, null, null);
        if (mCursor != null) {
            mCursor.moveToFirst();
        }
        return mCursor;
 
    }
 

FetchAllQuotes запускает запрос к БД и захватывает идентификаторы ID и поле цитат, возвращая эти результаты курсору. Первая переменная - таблица базы данных, вторая представляет собой колонки, которые должен вернуть запрос, а третья - строки колонок для возврата, если такие есть. Четвертая переменная - аргументы выбора, пятая - SQL-функция 'group by', шестая - SQL-оператор 'having', и седьмая - SQL-функция 'order by'. Только первые две переменные имеют значение, остальные равны нулю. В fetchQuote использована та же функция, но с определением строки, которую она обрабатывает.

 
    public boolean updateQuote(long rowId, String title) {
        ContentValues args = new ContentValues();
        args.put(KEY_QUOTES, title);
 
        return mDb.update(DATABASE_TABLE, args, KEY_ROWID + "=" + rowId, null) > 0;
    }
    

Для оператора обновления (update) нам еще нужно название базы данных, новые переменные для каждой данной строки и, наконец, номер строки, в которую вносить обновление.

 
    public int getAllEntries()
    {
        Cursor cursor = mDb.rawQuery(
                    "SELECT COUNT(quotes) FROM tblRandomQuotes", null);
                if(cursor.moveToFirst()) {
                    return cursor.getInt(0);
                }
                return cursor.getInt(0);
 
    }
 
    public String getRandomEntry()
    {
    	int id = 1;
    	id = getAllEntries();
 
    	int rand = random.nextInt(id) + 1;
        Cursor cursor = mDb.rawQuery(
                    "SELECT quotes FROM tblRandomQuotes WHERE _id = " + rand, null);
                if(cursor.moveToFirst()) {
                    return cursor.getString(0);
                }
                return cursor.getString(0);
 
    }
}

Две функции, описанные выше, упоминались в предыдущем посте. Здесь они используются для генерации случайной цитаты на экране в виде toast-а.Далее мы проработаем все .xml файлы, начиная со strings.xml. Он будет содержать значения стринговых переменных для всех трех XML-файлов макета. Код будет очень схож с несколькими предыдущими примерами. Содержимое файла strings.xml:

 
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Quotes Tracker</string>
    <string name="no_quotes">No Quotes Yet</string>
    <string name="menu_insert">Add Quote</string>
    <string name="menu_delete">Delete Quote</string>
    <string name="title">Quote:</string>
    <string name="confirm">Confirm</string>
    <string name="edit_quotes">Edit Quote</string>
    <string name="genRan">Generate Random Quote!</string>
</resources>

После strings.xml идем дальше, к файлу row.xml в папке макетов (layout). Этот файл еще не создан, так что придется создать новый XML-файл. Нужно правой кнопкой мышки кликнуть на папке layout, перейти к New и выбрать Other… Предложенный список нужно прокрутить вниз, пока не появится папка XML. Откройте ее и двойным кликом мыши нажмите на файл под названием XML. Измените имя файла с NewFile.xml на row.xml. После этого файл будет создан, появится сообщение об ошибке, которую мы исправим позже. Далее представлен код, который понадобится вставить в XML-файл:

<?xml version="1.0" encoding="utf-8"?>
<TextView android:id="@+id/text1" xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

Исходный код для этого макета представляет собой метку или TextView, которая будет многократно вставлять в main.xml каждую нашу введенную запись. Перейдем к main.xml, чтобы посмотреть, как это реализуется.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<ListView android:id="@+id/android:list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/android:empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No Quotes!"/>
<Button
android:id="@+id/genRan"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/genRan"
/>
</LinearLayout>

В коде выше используется LinearLayout и ListView, а также одиночная метка, отображающая текст “No Quotes!” («Нет цитат!»), на случай, если база данных пуста. Простого отображения записей БД нам мало. Нужно чтобы одна запись генерировалась случайным образом. Это осуществляется нажатием кнопки, действие описано в конце кода ListView. Теперь нужно перейти к файлу edit.xml. Его нужно создать аналогично предыдущему файлу:

<?xml version="1.0" encoding="utf-8"?>
 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent"
	android:layout_height="fill_parent">
 
	<LinearLayout android:orientation="horizontal"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content">
 
		<TextView android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:text="@string/title" />
		<EditText android:id="@+id/title"
		  android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:layout_weight="1"/>
	</LinearLayout>
 
	<Button android:id="@+id/confirm"
	  android:text="@string/confirm"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content" />
 
</LinearLayout>

В этом фрагменте кода идут два компонента макета LinearLayout друг за другом. Для получения четкой и ровной компоновки элементов, используются два менеджера компоновки. Первый выравнивает элементы по вертикали и заполняет родительское окно. Второй выравнивает текстовое поле и метку по горизонтали. Без этих двух линейных компоновщиков текстовое поле будет размером с целое окно вместо одной строки. Не считая двойной линейной компоновки, код макета стандартный, и трудностей с ним быть не должно.

Далее создадим новый .java файл в пакете om.gregjacobs.enhancedquotes под названием QuoteEdit. Он будет содержать код для осуществления правки введенного текста. Ниже приведен сам код с комментариями к тем местам, которые вы можете не знать. В целом, почти все должно быть уже знакомо, так как мы уже использовали эти функции и методы в предыдущих постах. QuoteEdit.java содержит следующий код:

package com.gregjacobs.enhancedquotes;
 
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
 
public class QuoteEdit extends Activity {
 
	private EditText mQuoteText;
    private Long mRowId;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.edit);
 
        mQuoteText = (EditText) findViewById(R.id.title);
 
        Button confirmButton = (Button) findViewById(R.id.confirm);
 
        mRowId = null;
        Bundle extras = getIntent().getExtras();
        if (extras != null) {
            String title = extras.getString(QuotesDBAdapter.KEY_QUOTES);
            mRowId = extras.getLong(QuotesDBAdapter.KEY_ROWID);
 
            if (title != null) {
            	mQuoteText.setText(title);
            }
        }
       

Этот фрагмент абсолютно стандартный до части, содержащей Bundle extras = getIntent().getExtras();. Эта часть кода вынимает содержимое из QuotesMain.java, используя Intent. Новичкам наверняка интересно, что такое Intent. Intent - это пассивный объект, содержащий данные и способный передавать их между приложениями. Говоря человеческим языком, это своего рода буфер, позволяющий быстро и легко достать информацию из QuotesMain.java и передать его в файл QuotesEdit.java. Еще один новый термин - Bundle. Bundle позволяет передавать строки в объекты по тому же принципу, что и Intent. Используя Bundle extras, мы реализовали передачу данных из файла main .java в QuotesEdit.java и наоборот.

        confirmButton.setOnClickListener(new View.OnClickListener() {
 
            public void onClick(View view) {
                Bundle bundle = new Bundle();
 
                bundle.putString(QuotesDBAdapter.KEY_QUOTES, mQuoteText.getText().toString());
                if (mRowId != null) {
                    bundle.putLong(QuotesDBAdapter.KEY_ROWID, mRowId);
                }
 
                Intent mIntent = new Intent();
                mIntent.putExtras(bundle);
                setResult(RESULT_OK, mIntent);
                finish();
            }
 
        });
    }
}

Функция Bundle присваивает тексту, введенному в текстовое поле, ID-идентификатор, и отправляет все вместе в файл QuotesMain.java через QuotesEdit.java. Теперь перейдем в файл QuotesMain.java, чтобы скомпоновать все воедино. Следующий фрагмент кода описывает реализацию длинного нажатия и присвоение кнопке меню любого телефона функции добавления. Этот код необходимо включить в файл QuotesMain.java:

package com.gregjacobs.enhancedquotes;
 
import android.app.ListActivity;
import android.view.View.OnClickListener;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.ListView;
import android.widget.Button;
import android.widget.SimpleCursorAdapter;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;

Выше указан ряд новых импортов, необходимых для реализации расширенного функционала программы, в частности, menu и menuitem, listview и simplecursoradapters. Эти и другие используемые элементы будут пояснены далее.

public class QuotesMain extends ListActivity {
    private static final int ACTIVITY_CREATE=0;
    private static final int ACTIVITY_EDIT=1;
 
    private static final int INSERT_ID = Menu.FIRST;
    private static final int DELETE_ID = Menu.FIRST + 1;
 
    private QuotesDBAdapter mDbHelper;
    private Cursor mNotesCursor;
 
    public Button button;
    /** Вызывается при первом создании процесса. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mDbHelper = new QuotesDBAdapter(this);
        mDbHelper.open();
        fillData();
        registerForContextMenu(getListView());
        button = (Button)findViewById(R.id.genRan);
        button.setOnClickListener(mAddListener);
    }
    

Мы прописываем переменные для создания, правки, вставки и удаления цитат. Это статичные переменные, так как их значения не меняются. В функции onCreate мы используем fillData(), который будет определен ниже. Мы также регистрируем элементы listview в контекстном меню и настраиваем слушатель событий для кнопки. Лучше всего описывать контекстное меню как всплывающее. Это и будет реализовано для случаев, когда мы хотим удалить цитату с помощью listview.

    private OnClickListener mAddListener = new OnClickListener()
    {
    	public void onClick(View v)
    	{
			//long id1 = 0;
			// производит действие при нажатии кнопки
			try
			{
				String quote = "";
				quote = mDbHelper.getRandomEntry();
				Context context = getApplicationContext();
				CharSequence text = quote;
				int duration = Toast.LENGTH_LONG;
 
				Toast toast = Toast.makeText(context, text, duration);
				toast.show();
			}
			catch (Exception ex)
			{
				Context context = getApplicationContext();
				CharSequence text = ex.toString();
				int duration = Toast.LENGTH_LONG;
 
				Toast toast = Toast.makeText(context, text, duration);
				toast.show();
			}
    	}
    };
 
    private void fillData() {
        // Создает список из всех строк БД
        mNotesCursor = mDbHelper.fetchAllQuotes();
        startManagingCursor(mNotesCursor);
 
        // Создает массив для определения полей, которые мы хотим отобразить в списке (только TITLE)
        String[] from = new String[]{QuotesDBAdapter.KEY_QUOTES};
 
        // и массив полей, которые мы хотим связать (в этом случае только text1)
        int[] to = new int[]{R.id.text1};
 
        // Сейчас создаем SimpleCursorAdapter и настраиваем его отображение
        SimpleCursorAdapter notes =
        	    new SimpleCursorAdapter(this, R.layout.row, mNotesCursor, from, to);
        setListAdapter(notes);
    }
    

Функция button в верхнем коде точь-в-точь повторяет предыдущую, которая генерирует случайную цитату из нашего списка. Новый метод fillData(), как упоминается выше, используется для привязки всех цитат к идентификаторам и добавления этих данных вместе с текущей цитатой в listview, используя SimpleCursorAdapter.

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        menu.add(0, INSERT_ID,0, R.string.menu_insert);
        return true;
    }
 
    @Override
    public boolean onMenuItemSelected(int featureId, MenuItem item) {
        switch(item.getItemId()) {
        case INSERT_ID:
            createNote();
            return true;
	    }
        return super.onMenuItemSelected(featureId, item);
    }

В первой функции onCreateOptionsMenu() вышеуказанного кода мы прописываем возможность добавить запись в БД, используя опцию меню 'press', которая вызывает диалоговое окошко с вопросом, хотим ли мы это сделать. Если ответ положительный, оператор возвращает значение true. Следующая функция проверяет, была ли нажата опция в меню. Если да, оператор switch проверяет значение, какой ответ дал пользователь. Если оно положительное, создастся новая запись.

    @Override
	public void onCreateContextMenu(ContextMenu menu, View v,
			ContextMenuInfo menuInfo) {
		super.onCreateContextMenu(menu, v, menuInfo);
        menu.add(0, DELETE_ID, 0, R.string.menu_delete);
	}
 
    @Override
	public boolean onContextItemSelected(MenuItem item) {
		switch(item.getItemId()) {
    	case DELETE_ID:
    		AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
	        mDbHelper.deleteQuote(info.id);
	        fillData();
	        return true;
		}
		return super.onContextItemSelected(item);
	}
 
	private void createNote() {
    	Intent i = new Intent(this, QuoteEdit.class);
    	 startActivityForResult(i, ACTIVITY_CREATE);
   	 }

Эта функция используется для регистрации контекстного меню и предоставления опции удаления записи, используя функцию menu.add, как показано выше. Если пункт контекстного меню под названием Delete нажат, запрос к БД удалит цитату, по её ID. Функция  createNote() использует интент для пропуска приложения через файл QuoteEdit и загрузки нового экрана, после создания которого новый intent вышлет обновленные данные обратно, чтобы мы могли добавить их в listview.

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
    	super.onListItemClick(l, v, position, id);
        Cursor c = mNotesCursor;
        c.moveToPosition(position);
        Intent i = new Intent(this, QuoteEdit.class);
        i.putExtra(QuotesDBAdapter.KEY_ROWID, id);
        i.putExtra(QuotesDBAdapter.KEY_QUOTES, c.getString(
                c.getColumnIndexOrThrow(QuotesDBAdapter.KEY_QUOTES)));
        startActivityForResult(i, ACTIVITY_EDIT);
    }

Если запись из listview нажата, вышеописанная функция загружается для инициализации intent-а, помещает информацию в intent и отправляет ее в файл QuoteEdit для правки класса. После завершения операции класс QuoteEdit обратно отсылает правленые данные, и мы опять можем добавлять, править или удалять цитаты.

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);
        Bundle extras = intent.getExtras();
        switch(requestCode) {
        case ACTIVITY_CREATE:
            String title = extras.getString(QuotesDBAdapter.KEY_QUOTES);
            mDbHelper.createQuote(title);
            fillData();
            break;
        case ACTIVITY_EDIT:
            Long rowId = extras.getLong(QuotesDBAdapter.KEY_ROWID);
            if (rowId != null) {
                String editTitle = extras.getString(QuotesDBAdapter.KEY_QUOTES);
                mDbHelper.updateQuote(rowId, editTitle);
            }
            fillData();
            break;
        }
    }
}

Метод, описанный выше, захватывает результат процесса и использует его для выбора определенного метода. Результатом в данном случае будет либо создание новой цитаты либо правка существующей. Базой для этого оператора switch будет использование помощника БД для вставки или обновления данных базы данных.Далее необходимо создать еще один файл перед тем, как запустить приложение на эмуляторе. Это будет файл AndroidManifest.XML, который будет управлять ходом работы программы. Это, собственно и будет ядром нашего приложения, необходимым для распознавания его обеих частей. Это код файла AndroidManifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.gregjacobs.enhancedquotes">
    <application android:icon="@drawable/icon">
        <activity android:name=".QuotesMain" android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    	<activity android:name=".QuoteEdit" android:label="@string/app_name"></activity>
    </application>
</manifest>

Если ваш код не совпадает с указанным выше, его необходимо доработать. Заметно, что мы добавили новый Activity в файл «manifest», дав ему такое же название, как предыдущему, app_name. Таким образом мы обозначили еще один процесс, который хотим использовать. Также вы заметите, что в коде убрана строка uses-sdk android:minSdkVersion="3". Это сделано из-за того, что приложение действительно в нем не нуждается.

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

Вот для этого и нужен Dalvick Debug Monitor Server (DDMS). Когда эмулятор запущен, можно переключиться на DDMS, нажав >> в правом верхнем углу экрана и выбрав DDMS. Если вы новичок в разработках под Android, вполне возможно, что отображаемая в DDMS информация будет не совсем понятной. В данном случае DDMS будет использоваться для захвата базы данных из работающего эмулятора.

Вначале нужно установить программу, которую лично я считаю очень полезной для SQLite разработчиков. Называется она SQLite Database Browser (SDB). Это ПО позволяет открывать базы данных SQLite и исследовать их содержимое, и даже изменять данные, используя SQL операторы. После загрузки файла найдите его папку, кликните на .exe файле и запустите его. Оставьте эту программку открытой, мы вернемся к ней позже.

Для того, чтобы поместить их в SDB, нам необходимо изъять их из эмулятора. Для этого нам нужно влезть в работающий эмулятор и отыскать нужную нам базу данных. Нужно помнить, что базы данных являются специализированными, так что нужно найти имя пакета, в котором база данных расположена в папке Database Folder. Во время работы с DDMS нужно перейти на панель устройств и кликнуть на нужном эмуляторе. Далее где-то в середине окна программы должна быть вкладка под названием File Explorer. Кликнув ее, увидите появившиеся три папки (может и больше в зависимости от того, для чего используется устройство). Эти три папки называются data, sdcard и system. Открываем папку data. В ней должна быть еще одна папка с названием data, ее тоже открываем. В этой папке содержатся названия всех пакетов, установленных на эмуляторе. Переходим на com.gregjacobs.enhancedquotes и открываем его. Видим две папки - databases и lib. Открываем databases и выбираем файл Random. Нужно кликнуть по нему, затем вверху вкладки нажать кнопочку в виде флоппи-диска со стрелкой, указывающей влево. При нажатии этой кнопки появится диалоговое окно, спрашивающее, куда вы хотите сохранить выбранный файл. Выберете удобное для вас место и кликните save (сохранить).

После того, как мы извлекли файл из эмулятора, возвращаемся в SDB и нажимаем большую кнопку под названием open (открыть), находим наш сохраненный файл и кликаем 'open'. Теперь мы можем увидеть структуру БД и просмотреть ее данные. Для этого нужно кликнуть на вкладке  под названием Browse Data и в выпадающем меню выбрать tblRandomQuotes. Данные БД появятся на экране. Теперь вы знаете, где найти ваши данные, если понадобится их изменить и залить обратно в эмулятор. Программа SDB также полезна для тестирования SQL-запросов, если вы не уверены в том, какие результаты они будут возвращать. В целом, это бесценный инструмент для тех, кто программирует приложения с БД под Android.

Это ссылки на файлы моего проекта, для сравнения:

AndroidManifest.xml | edit.xml | main.xml | QuoteEdit.java | QuoteMain.java | QuotesDBAdapter.java | row.xml | strings.xml

После освоения этого поста у вас должно появиться уже более-менее продвинутое понимание некоторых опций графического интерфейса и программирования баз данных. Теперь вы смело можете приступать к созданию более глубоких приложений, чем простой набор полей и кнопок. Умея использовать инструменты Intent и Bundle, вы можете структурировать свои приложения, разделяя код и макеты на части для лучшей работы и соответствия вашим целям. Если кому-то в голову пришли интересные идеи в ходе освоения этого поста, высылайте их в комментариях, обсудим их вместе. Следующий пост будет посвящен созданию сборщика статистики, а также построению пользовательского интерфейса с помощью DroidDraw.

До следующего поста!