Поиск по этому блогу

четверг, 16 ноября 2017 г.

Разработка базы данных с помощью Room


Все сталкиваются рано или поздно с созданием локальной базы данных в своих приложениях. Часто, после нескольких часов гугления у нас остается список из кучи ORM которые могут нам помочь с разработкой БД. Я уже писал про самостоятельную разработку БД, еще очень давно, с того времени много чего поменялось, мой уровень знаний вырос, и я сейчас бы не советовал использовать тот способ, так как я писал уже про работу с БД с помощью Realm который разы удобней и проще чем написание и поддержка базы на стандартных методах и классах андроида. Тем более Room является библиотекой которую сам Google советует использовать как БД.

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


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

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

Добавляем в app/build.gradle наши библиотеки. А еще по недавней традиции добавляем java 8 в проект, так будет красивее. 

app/build.gradle
android {
    ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support:recyclerview-v7:26.1.+'

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

    implementation "android.arch.persistence.room:runtime:1.0.0"
    annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
}

Как видно из списка dependencies, у нас подключен RecyclerView для отображения списка, Butter Knife для простого доступа к вьюхам, и сам Room для создания БД. Вот и все, у нас есть все что нам нужно для создания красоты. 

Начнем мы с того что создадим модель которая будет иметь поля в которые мы будем сохранять данные. 

DataModel.java
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;

@Entity
public class DataModel {

    @NonNull
    @PrimaryKey
    private String title;
    private String description;

    @NonNull
    public String getTitle() {
        return title;
    }

    public void setTitle(@NonNull String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

Вот такая моделька у нас будет. В ней у нас будет тайтл который в то же время будет у нас @PrimaryKey для сохранения связей между таблицами, но так как у нас таблица одна нам пока связывать ничего особо не нужно, и еще у нас будет поле описание. Так же у нас вверху над классом стоит аннотация @Entity, она значит для Room что этот класс будет использовать как таблица в БД.

Дальше нам нужно создать интерфейс Dao который будет расказывать Room что мы будем делать с нашей моделью (таблицей). Для этого мы создаем интерфейс и определяем в нем методы которые нам нужны для работы с БД, в моем случае это будет запись, удаление и чтение.

DataDao.java
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.Query;

import com.project.dajver.roomdatabaseexample.db.model.DataModel;

import java.util.List;

@Dao
public interface DataDao {
    @Insert
    void insert(DataModel dataModel);

    @Delete
    void delete(DataModel dataModel);

    @Query("SELECT * FROM DataModel")
    List<DataModel> getAllData();

    //пример запроса с выборкой
    @Query("SELECT * FROM DataModel WHERE title LIKE :title")
    List<DataModel> getByTitle(String title);
}

Как видно из этого интерфейса, мы определили этот интерфейс аннотацией @Dao, она объясняет Room что мы будем делать с таблицей, мы в нее можем записать данные, удалить их и получить список всех данных. Каждый метод мы инициализируем аннотациями которые указывают то или иное действие. @Insert — очевидно значит запись в БД. @Delete — очевидно удаление. и @Query() — у нас выполняет действия по выполнению SQL запросов к БД, если захотите закостамизировать какие-то реквесты, на пример поиск по БД, вам достаточно просто вписать SQL в эту аннотацию и в зависимости от параметров которые вы там укажите, Room вернет вам ваши данные из БД.

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

DatabaseHelper.java
@Database(entities = { DataModel.class }, version = 1, exportSchema = false)
public abstract class DatabaseHelper extends RoomDatabase {

    public abstract DataDao getDataDao();

    @Override
    protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config) {
        return null;
    }

    @Override
    protected InvalidationTracker createInvalidationTracker() {
        return null;
    }
}

В этом классе мы указали в аннотации @Database, что у нас будет использоваться класс DataModel как таблица в которой будут храниться данные. version = 1 — у нас значит версию базы данных, при обновлении БД нужно будет только увеличивать версию и никаких сложных действий больше делать не придется, все остальное Room сделает сам. exportSchema = false — я использовал для того что бы не было постоянных ворнингов что схема не может быть построена или сохраненна. По сути каждый раз когда вы создаете БД создается файл схемы БД в JSON, и каждый раз при обновлении БД оно создает ее бекап что бы можно было видеть что было в старой и что появилось в новой. Детальней можно прочесть тут на стеке, может кому-то эта функция понадобится.

Ну и собственно наш единственный абстрактный метод abstract DataDao getDataDao() который возвращает все методы по БД которые у нас созданны в интерфейсе Dao.

Теперь нам нужно создать инстанс БД в синглтоне, не создавать же нам его каждый раз. По этому я выбрал класс Application который создается во время первого запуска приложения, и живет все время пока приложение работает. В нем я создал инстанс Room, а точней инстанс нашего DatabaseHelper который мы создали ранее.

App.java
import android.app.Application;
import android.arch.persistence.room.Room;

import com.project.dajver.roomdatabaseexample.db.DatabaseHelper;

public class App extends Application {

    private static App instance;
    private DatabaseHelper db;

    public static App getInstance() {
        return instance;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
        db = Room.databaseBuilder(getApplicationContext(), DatabaseHelper.class, "data-database")
                .allowMainThreadQueries()
                .build();
    }

    public DatabaseHelper getDatabaseInstance() {
        return db;
    }
}

Все что происходит вокруг этого класса я думаю можно не описывать, хочу только остановиться на методе onCreate() в котором у нас создается объект класса DatabaseHelper. А точнее мы создаем его экземпляр с помощью Room.databaseBuilder, и называем его каким-то своим произвольным названием которое вам будет хотеться его назвать в моем случае это data-database. Приставка database не обязательна, это просто для примера. Ну и allowMainThreadQueries() разрешает нам делать запросы сразу в UI потоке без лишних обработчиков.

Далее создадим адаптер в котором будем отображать данные из БД.

SomeDataRecyclerAdapter.java
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.project.dajver.roomdatabaseexample.R;
import com.project.dajver.roomdatabaseexample.db.model.DataModel;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;

public class SomeDataRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

    private List<DataModel> dataModels = new ArrayList<>();
    private OnDeleteListener onDeleteListener;
    private Context context;

    public SomeDataRecyclerAdapter(Context context, List<DataModel> dataModels) {
        this.context = context;
        this.dataModels = dataModels;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_some_data, parent, false);
        return new NewsViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
        final NewsViewHolder viewHolder = (NewsViewHolder) holder;
        viewHolder.title.setText(dataModels.get(position).getTitle());
        viewHolder.description.setText(dataModels.get(position).getDescription());
    }

    @Override
    public int getItemCount() {
        return dataModels.size();
    }

    public class NewsViewHolder extends RecyclerView.ViewHolder {

        @BindView(R.id.title)
        public TextView title;
        @BindView(R.id.description)
        public TextView description;
        @BindView(R.id.delete)
        public TextView delete;

        public NewsViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            delete.setOnClickListener(view -> {
                onDeleteListener.onDelete(dataModels.get(getAdapterPosition()));
                dataModels.remove(getAdapterPosition());
                notifyItemRemoved(getAdapterPosition());
            });
        }
    }

    public void setOnDeleteListener(OnDeleteListener onDeleteListener) {
        this.onDeleteListener = onDeleteListener;
    }

    public interface OnDeleteListener {
        void onDelete(DataModel dataModel);
    }
}

Вполне себе стандартный адаптер. В него мы передаем список с DataModel и дальше в onBindViewHolder биндим эти данные во вьюхи. Так же у нас есть колбек который возвращает клик по крестику и вовзращает этот колбек в активити для удаления айтема из списка. Я даже не знаю что тут еще описывать еще. По идее все уже должны быть знакомы с этим адаптером, это вроде как стандарт.

Ну и файл разметки для адаптера.

item_some_data.xml
<?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="wrap_content"
    android:orientation="vertical"
    android:padding="10dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="0.1"
            android:orientation="vertical">

            <TextView
                android:id="@+id/title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="TextView"
                android:textColor="@android:color/black"
                android:textSize="18sp" />

            <TextView
                android:id="@+id/description"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text="TextView" />
        </LinearLayout>

        <TextView
            android:id="@+id/delete"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="[X]"
            android:textColor="@android:color/black"
            android:textSize="18sp" />

    </LinearLayout>

</LinearLayout>

А теперь осталось написать активити с списком и активити добавления. Начнем мы с главного экрана со списком. 

MainActivity.java
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuItem;

import com.project.dajver.roomdatabaseexample.App;
import com.project.dajver.roomdatabaseexample.R;
import com.project.dajver.roomdatabaseexample.db.DatabaseHelper;
import com.project.dajver.roomdatabaseexample.db.model.DataModel;
import com.project.dajver.roomdatabaseexample.ui.AddDataActivity;
import com.project.dajver.roomdatabaseexample.ui.main.adapter.SomeDataRecyclerAdapter;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity implements SomeDataRecyclerAdapter.OnDeleteListener {

    @BindView(R.id.recyclerView)
    RecyclerView recyclerView;

    private DatabaseHelper databaseHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL,false));
        databaseHelper = App.getInstance().getDatabaseInstance();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_add_button, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_add: {
                startActivity(new Intent(this, AddDataActivity.class));
                break;
            }
        }
        return false;
    }

    @Override
    protected void onResume() {
        super.onResume();
        SomeDataRecyclerAdapter recyclerAdapter = new SomeDataRecyclerAdapter(this, databaseHelper.getDataDao().getAllData());
        recyclerAdapter.setOnDeleteListener(this);
        recyclerView.setAdapter(recyclerAdapter);
    }

    @Override
    public void onDelete(DataModel dataModel) {
        databaseHelper.getDataDao().delete(dataModel);
    }
}

В onCreate() мы создали инстанс DatabaseHelper что бы можно было получать данные из БД и удалять их, и указали RecyclerView какой LayoutManager ему использовать. В onCreateOptionsMenu() и onOptionsItemSelected() мы делаем менюшку в тулбаре. В onResume() создаем адаптер, и каждый раз когда мы удем возвращаться с экрана добавления у нас будет обновленный адаптер с внесенными туда данными. Ну и onDelete() который по клику удаляет айтем из списка и БД.

Разметка активити и файл с пунктом меню для тулбара будет выглядеть так:

activity_main_xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="com.project.dajver.roomdatabaseexample.ui.main.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

menu_add_button.xml
<?xml version="1.0" encoding="utf-8"?>
<menu
    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"
    tools:context=".activity.MainActivity">

    <item
        android:id="@+id/action_add"
        android:title="Add"
        app:showAsAction="always"/>
</menu>

Как видно из кода сверху у нас все тривиально. Список в мейн активити, и кнопка в меню которая видна всегда по умолчанию.

Дальше давайте сделаем экран добавления. В нем у нас будет как я говорил ранее — два поля и кнопка для добавления. После добавления экран закрывается.

AddDataActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.EditText;

import com.project.dajver.roomdatabaseexample.App;
import com.project.dajver.roomdatabaseexample.R;
import com.project.dajver.roomdatabaseexample.db.DatabaseHelper;
import com.project.dajver.roomdatabaseexample.db.model.DataModel;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class AddDataActivity extends AppCompatActivity {

    @BindView(R.id.title)
    EditText title;
    @BindView(R.id.description)
    EditText description;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_add);
        ButterKnife.bind(this);
    }

    @OnClick(R.id.save)
    public void onSaveClick() {
        DatabaseHelper databaseHelper = App.getInstance().getDatabaseInstance();

        DataModel model = new DataModel();
        model.setTitle(title.getText().toString());
        model.setDescription(description.getText().toString());
        databaseHelper.getDataDao().insert(model);

        finish();
    }
}

Единственное что нас тут интересует это onSaveClick(), в нем мы создаем инстанс DatabaseHelper, а дальше заполняем нашу модель DataModel, и передаем этот объект на запись в insert. И завершаем активити. 

Разметка для класса добавления.

activity_add.xml
<?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="match_parent"
    android:orientation="vertical"
    android:padding="10dp">

    <EditText
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:ems="10"
        android:hint="Title"
        android:inputType="textPersonName" />

    <EditText
        android:id="@+id/description"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:ems="10"
        android:hint="Description"
        android:inputType="textPersonName" />

    <Button
        android:id="@+id/save"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:onClick="onSaveClick"
        android:text="Save" />
</LinearLayout>

Осталось не забыть добавить активити в манифест, и начать компиляцию. 

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

Исходники:
GitHub