Разработка Android приложений. Работа с SQLite

4.8 (3)

Иногда при разработке Андроид приложения возникает необходимость создавать и хранить данные непосредственно на устройстве. Причем так, чтобы  они сохранялись не только во время цикла работы приложения. Можно было придумать самолично много всякого, но в этом нет смысла т.к. в Андроиде есть возможность использовать SQLite. SQLite это легковесная база данных, которая достаточно часто применяется для хранения данных, она настолько мала что применяется для хранения данных в приложении. Давайте разберем на примере элементарного приложения как с можно работать SQLite.

 

Перед началом имеет смысл добавить библиотеку для упрощенной привязки видов к переменным в коде используя аннотации. Эта библиотека называется ButterKnife. Для этого в build.gradle модуля app в dependencies допишем следующие 2 строчки:


compile 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

Теперь создадим пару строк в ресурсах проекта(res/values/strings.xml):

<resources>
  <string name="app_name">SQLiteTutorial</string>
  <string name="welcome">Welcome to SQLite tutorial by SIDStudio</string>
  <string name="hint_name">Name</string> <string name="hint_phone_number">Phone number</string>
  <string name="hint_line">Line to remove</string>
  <string name="button_add">Add</string>
  <string name="button_remove">Remove</string>
  <string name="dialog_index_title">Type row index you want to remove</string>
  <string name="dialog_button_remove_line">Remove line</string>
  <string name="dialog_button_clear_database">Clear database</string>
  <string name="info_records_added">New record added:nName: %1$s, Number: %2$s</string>
  <string name="info_records_removed_line">Line #%1$s removed</string>
  <string name="info_records_database_cleared">All records cleared</string>
  <string name="info_field_must_be_filled">Field must be filled</string>
  <string name="info_input_error_fields_empty">All fields must be filled</string>
</resources>

Тут всё что нам может понадобиться в этой статье. Ну теперь создадим внешний вид нашего Activity:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context="sidstudio.com.sqlitetutorial.MainActivity">
   <TextView
       android:id="@+id/main_text_welcome"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="16dp"
       android:text="@string/welcome"
       android:textAlignment="center"
       android:textColor="#000"
       android:textSize="18sp"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp" />
   <LinearLayout
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="8dp"
       android:orientation="horizontal"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/main_text_welcome"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp"
       android:id="@+id/main_layout_fields">
       <EditText
           android:id="@+id/main_edit_name"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_weight="2"
           android:ems="10"
           android:hint="@string/hint_name"
           android:inputType="textPersonName" />
       <EditText
           android:id="@+id/main_edit_number"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_weight="1"
           android:ems="10"
           android:hint="@string/hint_phone_number"
           android:inputType="phone" />
   </LinearLayout>
   <LinearLayout
       android:id="@+id/main_layout_buttons"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="8dp"
       android:orientation="horizontal"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/main_layout_fields"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp">
       <Button
           android:id="@+id/main_button_add"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_weight="1"
           android:text="@string/button_add" />
       <Button
           android:id="@+id/main_button_remove"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_weight="1"
           android:text="@string/button_remove" />
   </LinearLayout>
   <ScrollView
       android:layout_width="0dp"
       android:layout_height="0dp"
       android:layout_marginBottom="8dp"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="8dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintHorizontal_bias="0.0"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/main_layout_buttons">
       <TextView
           android:id="@+id/main_text_database"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:layout_constraintLeft_toLeftOf="parent"
           app:layout_constraintRight_toRightOf="parent"
           app:layout_constraintTop_toBottomOf="@+id/main_layout_buttons" />
   </ScrollView>
</android.support.constraint.ConstraintLayout>

Крайнее что мы пока что сделаем для подготовки - это привяжем виды к переменным с которыми будем работать используя ButterKnife. Мы не прописываем кнопки для этого только потому, что нам нужно от них будет только OnClick события, которые мы определим проще с помощью того-же ButterKnife, когда будем описывать для них методы работы с базой данных. Итак, наша заготовка на будущее в MainActivity:

public class MainActivity extends AppCompatActivity {
   @BindView(R.id.main_edit_name)
   EditText mEditName;
   @BindView(R.id.main_edit_number)
   EditText mEditPhoneNumber;
   @BindView(R.id.main_text_database)
   TextView mTextDatabase;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       //Инициализируем активити
       initialize();
   }

   private void initialize(){
       ButterKnife.bind(this);
   }
}

В метод initialize() мы будем добавлять другие фрагменты кода, которые нужны будут при создании activity. Методом ButterKnife.bind() мы говорим ButterKnife привязать все поля по аннотациям к видам у этого активити. Заготовка есть, теперь можно приступать к основному.

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

В андроиде работа с SQLite осуществляется с помощью применения класса SQLiteOpenHelper. Тут мы создадим свой, который наследует этот класс для удобства работы. Эта база данных должна будет уметь:

  • Создавать базу данных если таковой не существует.
  • Записывать значения в базу данных.
  • Собирать все значения в неё
  • Удалять указанную линию
  • Стирать все данные

Но это еще не всё. Эта база данных будет хранить в себе класс, который будет удобно в виде объекта показывать отдавать нам данные. Перво наперво создадим наш класс SQLiteContactHelper и сразу зададим пару констант:

 


public class SQLiteContactHelper extends SQLiteOpenHelper {
   //Тэг для написания логов
   private static final String TAG = "SQLTutorialHelper_TAG";
   //Название таблицы, которую будем создавать
   private static final String TABLE_NAME = "contacts";
   //Название колонок к этой таблице
   private static final String COL_ID = "id";
   private static final String COL_NAME = "name";
   private static final String COL_PHONE = "phone";



   public SQLiteContactHelper(Context context){
       //Вызываем стандартный конструктор в котором указываем контекст, имя таблицы и её версию;
       super(context, TABLE_NAME, null, 1);
   }

   @Override
   public void onCreate(SQLiteDatabase db) {
       //При создании объекта
   }

   @Override
   public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
       //При обнаружении изменения версии таблицы
   }
}

В потомке SQLiteOpenHelper обязательно должны быть реализованы методы onCreate и onUpgrade и обозначен конструктор. Метод onCreate выполняет действие при создании объекта, а второй при обнаружении разницы версии таблиц. В первом случае чаще всего создается таблица если таковой не существует. Среди констант мы задали все названия колонок, которые нам нужны. А теперь давайте создадим еще внутри этого класса, класс, который будет предоставлять нам база данных и который мы будем обрабатывать в базе данных и удобно его получать. Назовем этот класс ContactData:


public static class ContactData{
   public String name;
   public String phone;

   public ContactData(String name, String phone){
       this.name = name;
       this.phone = phone;
   }
}

Как видите, этот класс хранит поля name и phone, что будет аналогично колонкам в нашей таблице. Для старта только нужно создать таблицу с помощью SQLite запроса, который выполняется методом execSQL у объекта SQLiteDatabase, который мы применим из метода onCreate:


@Override
public void onCreate(SQLiteDatabase db) {
   db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + "(" +
           COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
           COL_NAME + " TEXT NOT NULL," +
           COL_PHONE + " TEXT UNIQUE NOT NULL" + ");");
}

Теперь перейдем к записи. Для записи потребуется класс ContentValues, с помощью которого мы будем передавать значения в базу данных и метод insertOrThrow(), который мы возьмем из SQLiteDatabase, до которого из метода достучаться через getWritableDatabase(). Выглядит метод следующим образом:


public void addContactData(ContactData contactData) throws SQLException{
   //Создаём набор значений для передачи в базу данных
   ContentValues contentValues = new ContentValues();
   //Кладём значения
   contentValues.put(COL_NAME, contactData.name);
   contentValues.put(COL_PHONE, contactData.phone);
   //Посылаем запрос на добавление записи
   long rowId = getWritableDatabase().insertOrThrow(TABLE_NAME, null, contentValues);
   //Выводим в консоль информацию о добавленной записи
   Log.d(TAG, "Added record:n-name: " + contactData.name + "n-phone: " + contactData.phone + "n-rowid:" + rowId);
}

insertOrThrow() - в отличии от просто метода insert(), он при ошибке SQL запроса выкидывает SQLException который мы будем в процессе обрабатывать. rowId это id ряда созданной записи. Всё что нам надо теперь это только считывать данные с таблицы. Для этого мы будем применять метод query() из того же объекта базы данных:


public ArrayList<ContactData> getAllContactData(){
   //Получаем курсор из запроса
   Cursor cursor = getWritableDatabase().query(TABLE_NAME, null, null, null, null, null, null);
   //Создадим arraylist куда будем добавлять объекты с данными
   ArrayList<ContactData> result = new ArrayList<>();

   //Метод двигает этот курсор на первую строчку и возвращает true. А если её нет, то возвращает false
   if(cursor.moveToFirst()){
       //Получаем id полей у текущего выбранного ряда.
       int rowName = cursor.getColumnIndex(COL_NAME);
       int rowPhone = cursor.getColumnIndex(COL_PHONE);
       //Проходим по элементам таблицы
       do{
           //Формируем объект ContactData на основе данных строчки таблицы, который сразу же добавляем в ArrayList
           result.add(new ContactData(cursor.getString(rowName), cursor.getString(rowPhone)));
       } while (cursor.moveToNext()); //Двигаемся до тех пор пока следующего элемента попросту не будет существовать, что означает конец таблицы.
   }
   //Возвращаем результат получения всех данных.
   return result;
}

Об объекте Cursor думайте как о курсоре в какой-либо таблице в Excel, которая вместо ячейки выбирает ряд. А для обращения к столбцу мы уже ищем непосредственно значение у ряда на который наведен курсор. Иными словами курсор получает все данные удовлетворяющие условиям из таблицы, и затем выборкой идет по столбцам из текущего выбранного ряда, который мы перемещаем через moveToFirst(), moveToLast(), move(int offset), moveToNext(), moveToPrevious(), moveToPosition(int position). Все эти методы возвращают boolean, который указывает существует ли ряд на который мы перемещаемся. Теперь надо бы элементы еще и очищать. Так как при очистке мы ничего не возвращаем - можно выполнить ручной SQLite запрос.


public void clearDatabase(){
   getWritableDatabase().execSQL("DELETE FROM " + TABLE_NAME + ";");
}

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


public void removePosition(int position) throws SQLException{
   getWritableDatabase().execSQL("DELETE FROM " + TABLE_NAME + " WHERE " + COL_ID + " IN " +
           "( SELECT " + COL_ID + " FROM " + TABLE_NAME + " LIMIT 1 OFFSET " + position + ");");
}

И вот теперь у нас есть полный спектр методов для простенькой работы с базой данных и создания связки с интерфейсом. Перво наперво для интерфейса создадим метод для обновления и отображения данных из таблицы в наш текст:



private void updateTable(){
   String data = "Tables data:";

   ArrayList<SQLiteContactHelper.ContactData> contacts = mSQLiteContactHelper.getAllContactData();
   for(int i = 0; i < contacts.size(); i++){
       data += "n" + i + ". " + "Name: " + contacts.get(i).name + "; Phone: " + contacts.get(i).phone + ";";
   }

   mTextDatabase.setText(data);
}

И вот с новым методом теперь мы будем показывать содержащее таблицы. Неплохо было бы это сделать при загрузке приложения:


@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   //Инициализируем активити
   initialize();
   updateTable();
}

И теперь давайте начнём придавать значение кнопкам. Начнём с кнопки Add и связывать OnClick мы будем опять же используя аннотации ButterKnife:



@OnClick(R.id.main_button_add)
void addContact(){
   //Инициализируем значения из полей ввода
   String name = mEditName.getText().toString();
   String phone = mEditPhoneNumber.getText().toString();
   //Если у нас нет пустых полей
   if(!name.isEmpty() && !phone.isEmpty()){
       //Создать контакт на основе введенных данных
       SQLiteContactHelper.ContactData contact = new SQLiteContactHelper.ContactData(name, phone);
       try{
           //Добавить его в базу данных и отобразить успех добавления данных в формате
           mSQLiteContactHelper.addContactData(contact);
           Toast.makeText(this, String.format(getString(R.string.info_records_added), name, phone), Toast.LENGTH_SHORT).show();
       } catch (SQLException ex){
           //Выдаём ошибку с самим сообщением об ошибке.
           Toast.makeText(this, ex.getMessage(), Toast.LENGTH_LONG).show();
       }
   } else {
       //Если данные не заполнены указать что мы не все поля заполнили
       Toast.makeText(this, getString(R.string.info_input_error_fields_empty), Toast.LENGTH_LONG).show();
   }
   //Обновляем отображение базы данных
   updateTable();
}

Тут достаточно просто - мы создаём объект основываясь на тексте в полях. Касаемо кнопки удаления нужно слегка повозится. На неё мы создадим диалоговое окно, которое будет просить ввести число ряда для удаления. Для этого создадим новый Layout файл:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical" android:layout_width="match_parent"
   android:layout_height="match_parent">
   <EditText
       android:id="@+id/dialog_index_edit"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:ems="10"
       android:inputType="number" />
</LinearLayout>

Это будет поле ввода в нашем диалоге. Собственно сам диалог мы создадим вручную и вызовем его при нажатии на кнопку удалении. В качестве выбора очистить все элементы или только конкретный - мы используем стандартные кнопки диалога:


@OnClick(R.id.main_button_remove)
void openRemoveDialog(){
   //Создаём наш layout через layout inflater
   final LinearLayout layout = (LinearLayout) getLayoutInflater().inflate(R.layout.dialog_index_input, null, false);
   //Настраиваем Alert Dialog под необходимые нам настройки
   final AlertDialog dialog = new AlertDialog.Builder(this)
           //Укажем что тот layout будет нашим видом в диалоге
           .setView(layout)
           .setTitle(R.string.dialog_index_title)
           .setPositiveButton(R.string.dialog_button_remove_line, new DialogInterface.OnClickListener() {
               @Override
               public void onClick(DialogInterface dialog, int which) {
                   //При нажати мы берем текст из вида, и получаем из него значение текста
                   EditText editIndex = (EditText)layout.findViewById(R.id.dialog_index_edit);
                   String textContext = editIndex.getText().toString();
                   //Если там написан текст
                   if(!textContext.isEmpty()){
                       //Пробуем удалить ряд из таблицы и при успехе выводим результат что ряд удалён.
                       try {
                           mSQLiteContactHelper.removePosition(Integer.valueOf(textContext));
                           Toast.makeText(MainActivity.this, String.format(getString(R.string.info_records_removed_line), textContext), Toast.LENGTH_LONG).show();
                       } catch (SQLException ex){
                           //При ошибке - выводим ошибку в toast
                           Toast.makeText(MainActivity.this, ex.getMessage(), Toast.LENGTH_LONG).show();
                       }
                   } else {
                       //При пустых полях, говорим что в полях должно быть значение
                       Toast.makeText(MainActivity.this, R.string.info_field_must_be_filled, Toast.LENGTH_LONG).show();
                   }
                   updateTable();
               }
           })
           .setNegativeButton(R.string.dialog_button_clear_database, new DialogInterface.OnClickListener() {
               @Override
               public void onClick(DialogInterface dialog, int which) {
                   //Очищаем базу данных, оповещаем что она успешно очищенна и обновляем её.
                   mSQLiteContactHelper.clearDatabase();
                   Toast.makeText(MainActivity.this, R.string.info_records_database_cleared, Toast.LENGTH_SHORT).show();
                   updateTable();
               }
           })
           .create();
   //Показываем наш созданный диалог пользователю.
   dialog.show();
}

Ну и вот у нас по сути готовое простенькое приложение с применением SQLite.

Комментарий (0)

Войдите с помощью соцсетей:
или
введите свои данные: