Если вы читаете эти строки, то, скорее всего, вам захотелось чего-то нового в отношениях с вашей базой данных. Вам надоел курсор, SQL запросы вы пишете каждый день и ощущение обыденности остудило те чувства, с которыми вы впервые сохраняли данные в БД. Что же, GreenDAO внесёт свежий поток в процесс разработки!
Библиотека от немецкой компании Greenrobot позиционируется как инструмент для объектно-реляционного отображения (ORM) данных в приложениях на Android, являясь прослойкой, реализующей шаблон проектирования DAO(Data Access Object). Если вы не знаете что это за шаблон, то многое в статье будет не понятно и лучше для начала прочитать эту статью. На момент написания статьи ребята выпустили уже версию 3.2.0, в которой побороли многие детские болячки и прихлопнули горсть багов. С помощью аннотаций происходит разметка сущности, требующей сохранения, создание DAO классов, таблиц и всю рутинную работу с ними библиотека взваливает на себя.
1.С чего начать?
Дружба начинается с улыбки, а использование библиотеки с её подключения в проект. В нашем случае одной строчкой в Gradle отделаться не получиться, всё чуть-чуть сложнее. Для начала идём в Gradle проекта и подключаем пути, добавляя строчку classpath ‘org.greenrobot:greendao-gradle-plugin:3.2.1’
Далее уже в Gradle файле модуля добавляем зависимости и подключаем плагин:
apply plugin: ‘org.greenrobot.greendao’
compile ‘org.greenrobot:greendao:3.2.0’
Плагин подключаем обязательно выше всех, в первой строчке. Баг это или фича – не нам судить. Ещё необходимо выполнить начальную конфигурацию GreenDAO, для чего добавляем блок
greendao{
schemaVersion 1
targetGenDir ‘src/main/java’
}
как указано на скриншоте:
Этот блок выполнит настройку GreenDAO. При тривиальных задачах можно не вникать в тюнинг библиотеки, но schemaVersion вы в любом случае должны будете указать и в случае её изменения инкрементировать это значение. Но если вам интересно, есть такие опции как:
schemaVersion – текущая версия схемы базы данных. Настройка используется OpenHelper’s классами для миграции между версиями схем. Как я и говорил, при изменении схемы делаете инкремент. По умолчанию значение 1.
daoPackage – Имя пакета для генерации DAOs, DaoMaster и DaoSession. По умолчанию – имя пакета, в котором будет находиться исходная сущность.
targetGenDir – место, где будут сгенерированы исходники. По умолчанию – директория проекта.
GenerateTests – можно задать true для автоматической генерации юнит-тестов.
targetGenDirTests – директория с тестами.
Всё, подключение выполнено.
2. Создание бизнес-модели.
Давайте теперь немного отвлечемся и посмотрим на то, что будем сохранять. У нас будет приложение – питомник котиков. Можно будет добавлять новых и отдавать в добрые руки старых котов. У котиков будут свойства — имя и порода. Давайте же опишем класс, не забыв добавить в него геттеры и сеттеры.
package info.javaway.greendaotutorial.model; public class Cat { private String breed; private String name; public Cat(String breed, String name) { this.breed = breed; this.name = name; } public String getBreed() { return breed; } public void setBreed(String breed) { this.breed = breed; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
Для наглядной демонстрации создадим список RecyclerView, который будет хранить в себе котиков. По нажатию кнопки будет создаваться новый котэ и добавляться в RecyclerView, по тапу на элемент списка будет происходить его удаление. Адаптер и холдер выглядят так:
package info.javaway.greendaotutorial; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.List; import info.javaway.greendaotutorial.model.Cat; public class CatAdapter extends RecyclerView.Adapter<CatAdapter.CatHolder> { private List<Cat> mCats; private LayoutInflater mInflater; public CatAdapter(List<Cat> cats, LayoutInflater inflater) { mCats = cats; mInflater = inflater; } @Override public CatHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = mInflater.inflate(R.layout.cat_item, parent, false); return new CatHolder(view); } @Override public void onBindViewHolder(CatHolder holder, int position) { Cat cat = mCats.get(position); holder.bindCat(cat); } @Override public int getItemCount() { return mCats.size(); } class CatHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private TextView mCatName; private TextView mCatBreed; public CatHolder(View itemView) { super(itemView); mCatName = (TextView) itemView.findViewById(R.id.cat_name); mCatBreed = (TextView) itemView.findViewById(R.id.cat_breed); itemView.setOnClickListener(this); } public void bindCat(Cat cat) { mCatName.setText(cat.getName()); mCatBreed.setText(cat.getBreed()); } @Override public void onClick(View view) { mCats.remove(getLayoutPosition()); notifyDataSetChanged(); } } }
В разметке всего 2 файла, для отображения элемента списка и главной активити:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_marginTop="8dp" android:id="@+id/create_cat_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <android.support.design.widget.TextInputLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="Введите имя" > <EditText android:id="@+id/cat_name_et" android:layout_width="match_parent" android:layout_height="wrap_content"/> </android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="Введите породу"> <EditText android:id="@+id/cat_breed_et" android:layout_width="match_parent" android:layout_height="wrap_content"/> </android.support.design.widget.TextInputLayout> </LinearLayout> <LinearLayout android:id="@+id/separator" android:layout_width="match_parent" android:layout_height="2dp" android:layout_below="@id/create_cat_layout" android:orientation="horizontal" android:background="@color/black"></LinearLayout> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_margin="16dp" android:src="@drawable/ic_add"/> <android.support.v7.widget.RecyclerView android:layout_below="@id/separator" android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPrimary" android:scrollbars="vertical" /> </RelativeLayout>
cat_item.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:layout_marginBottom="2dp" android:background="#FFFFFF" android:orientation="horizontal" android:padding="4dp" android:paddingBottom="8dp" android:paddingTop="8dp" > <TextView android:id="@+id/cat_name" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:layout_marginRight="4dp" android:layout_weight="1" android:background="#FFFFFF" android:textAlignment="center" android:textSize="18sp"/> <TextView android:id="@+id/cat_breed" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="4dp" android:layout_marginRight="8dp" android:layout_weight="1" android:background="#FFFFFF" android:textAlignment="center" android:textSize="18sp"/> </LinearLayout>
Код активити:
package info.javaway.greendaotutorial; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.EditText; import android.widget.Toast; import java.util.ArrayList; import java.util.List; import info.javaway.greendaotutorial.model.Cat; public class MainActivity extends AppCompatActivity { private FloatingActionButton mFab; private EditText mCatNameEditText; private EditText mCatBreedEditText; private RecyclerView mRecyclerView; private List<Cat> mCats; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView); mFab = (FloatingActionButton) findViewById(R.id.fab); mCatNameEditText = (EditText) findViewById(R.id.cat_name_et); mCatBreedEditText = (EditText) findViewById(R.id.cat_breed_et); mCats = new ArrayList<>(); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); final CatAdapter adapter = new CatAdapter(mCats, getLayoutInflater()); mRecyclerView.setAdapter(adapter); mFab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String name = mCatNameEditText.getText().toString(); String breed = mCatBreedEditText.getText().toString(); mCatNameEditText.setText(""); mCatBreedEditText.setText(""); mCatNameEditText.clearFocus(); mCatBreedEditText.clearFocus(); if(name.trim().equals("") || breed.trim().equals("")){ Toast.makeText(MainActivity.this, "Имя или порода не заполнены", Toast.LENGTH_SHORT).show(); return; } Cat cat = new Cat(name, breed); mCats.add(cat); adapter.notifyDataSetChanged(); } }); } }
Добавляем необходимые библиотеки в Gradle
compile ‘com.android.support:recyclerview-v7:25.3.0’
compile ‘com.android.support:design:25.3.0’
Ну вот, котики добавляются и по нажатию отдаются в новые семьи, добро восторжествовало. И теперь давайте займёмся сохранением данных, тем ради чего мы тут сегодня и собрались.
3.Подготовка к отображению в БД.
Библиотека использует аннотации, с помощью которых мы подготовим наших котиков к их отображению в базе данных. Добавим к классу нашего котика немного аннотаций, подготовим его к упаковке.
@Entity(active = true, nameInDb = "CATS") public class Cat { @Id private Long id; @NotNull private String breed; @NotNull private String name; public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public String getBreed() { return this.breed; } public void setBreed(String breed) { this.breed = breed; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } }
Что изменилось? Начнем сверху. Перед описанием класса находиться анноттация @Entity. Она сообщает GreenDAO что необходимо создать в базе данных таблицу с отображением помеченной сущности. Для этой аннотации можно задать дополнительные параметры:
@Entity( //Если у нас больше одной схемы, то необходимо сообщить, //к какой из них принадлежит объект. schema = "myschema", //Флаг делает сущность "active": активные сущности могут быть // обновлены, удалены, у них можно изменять методы active = true, // Имя таблицы в базе дынных // По умолчанию будет использовано имя класса nameInDb = "AWESOME_CATS", // Можно задать индексы сразу нескольким столбцам indexes = { @Index(value = "name DESC", unique = true) }, // Флаг, сообщающий GreenDAO создать таблицу. // Задайте false, если хотите много сущностей отобразить в // одной таблице или таблица создана снаружи уровня DAO createInDb = false, // Нужен ли конструктор со всеми свойствами? // Конструктор без аргументов нужен всегда. generateConstructors = true, // Генерировать ли геттеры и сеттеры, если они отсутствуют? generateGettersSetters = true ) public class Cat { ... }
Но в нашем случае можно было бы обойтись вообще без дополнительных параметров для аннотации, я их добавил для демонстрации.
Спускаемся ниже, где встречаем новое поле класса – id с аннотацией #Id. Так называемый суррогатный ключ. В бизнес модели он то нам не нужен, его судьба – быть первичным ключом в таблице, во что его и превращает аннотация @Id.
Поля breed и name помечены аннотациями @NotNull, что сообщает GreenDAO что в соответствующих столбцах не смогут храниться null значения. Данной аннотацией нельзя помечать примитивные поля, если такая необходимость возникла, то используйте классы-обёртки.
Есть ещё несколько аннотаций, одна из которых @Transient – исключает поле из процесса сохранения.
Переходим к самому интересному Хотите немного айтишной магии? Выполним сборку проекта(Build -> Make Project или Ctrl+F9). Проект собирается, но вы можете заметить, что мы теперь не одни, рядом с классом Cat теперь появились дополнительные классы, которые реализуют шаблон DAO. Но это ещё не всё, код нашего класса Cat также подвергся изменениям, а точнее дополнениям! Всё что сгенерировано дополнительно, помечается аннотацией @Generated. Разработчики настоятельно не рекомендуют править сгенерированный код, но если уж такая необходимость появляется, то замените после правки аннотацию @Generated на @Keep.
После первого сбора проекта мы можем пользоваться классами greenDAO.
DaoMaster – это точка входа для использования DreenDAO. DaoMaster содержит объект базы данных(SQLiteDatabase) и менеджеры DAO — классы, описывающие схемы. Также в нём есть статичные методы для создания и удаления таблиц. Его внутренние классы OpenHendler и DevOpenHendler являющиеся реализациями SQLiteOpenHendler, которые создают схему в базе данных SQLite.
DaoSession – управляет всеми доступными DAO объектами для описанной схемы, которые можно получить с помощью геттеров. DaoSession также предоставляет некоторые обобщенные методы, такие как insert, load, update, refresh и delete для сущностей.
CatDao – объект доступа к данным, сохраняющий и загружающий сущности. Для каждой сущности greenDao создает объект сущностьDao(Cat – CatDao, Man – ManDao, Client – ClientDao и т.п.). CatDao содержит уже расширенный список методов для работы с базой данных, например – count, loadAll и insertLnTx.
В контексте нашего примера мы можем создавать экземпляры DAO классов непосредственно перед использованием, на нашу архитектуру это никак не повлияет(так как её практически нет >_<). Но давайте всё же сделаем правильно и вынесем в отдельное место инициализацию. Для этого создадим класс App и унаследуемся от Application(не забудьте указать его файлом приложения в манифесте).
public class App extends Application { private DaoSession daoSession; @Override public void onCreate() { super.onCreate(); DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "mycats-db"); Database db = helper.getWritableDb(); daoSession = new DaoMaster(db).newSession(); } public DaoSession getDaoSession() { return daoSession; } }
манифест
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="info.javaway.greendaotutorial"> <application android:name=".App" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest>
Всё готово, теперь мы можем использовать классы DAO для сохранения и загрузки наших котиков. Финальная версия класса MainActivity:
package info.javaway.greendaotutorial; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.EditText; import android.widget.Toast; import org.greenrobot.greendao.query.Query; import java.util.ArrayList; import java.util.List; import info.javaway.greendaotutorial.model.Cat; import info.javaway.greendaotutorial.model.CatDao; import info.javaway.greendaotutorial.model.DaoSession; public class MainActivity extends AppCompatActivity { private FloatingActionButton mFab; private EditText mCatNameEditText; private EditText mCatBreedEditText; private RecyclerView mRecyclerView; private CatAdapter adapter; private List<Cat> mCats; public CatDao getCatDao() { return mCatDao; } private CatDao mCatDao; private Query<Cat> catsQuery; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setupViews(); DaoSession daoSession = ((App)getApplication()).getDaoSession(); mCatDao = daoSession.getCatDao(); catsQuery = mCatDao.queryBuilder().orderAsc(CatDao.Properties.Name).build(); updateCats(); } public void updateCats() { mCats = catsQuery.list(); adapter.setCats(mCats); } private void setupViews() { mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView); mFab = (FloatingActionButton) findViewById(R.id.fab); mCatNameEditText = (EditText) findViewById(R.id.cat_name_et); mCatBreedEditText = (EditText) findViewById(R.id.cat_breed_et); mCats = new ArrayList<>(); adapter = new CatAdapter(mCats, getLayoutInflater(), this); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mRecyclerView.setAdapter(adapter); mFab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { addCat(); } }); } private void addCat() { String name = mCatNameEditText.getText().toString(); String breed = mCatBreedEditText.getText().toString(); mCatNameEditText.setText(""); mCatBreedEditText.setText(""); mCatNameEditText.clearFocus(); mCatBreedEditText.clearFocus(); if (name.trim().equals("") || breed.trim().equals("")) { Toast.makeText(MainActivity.this, "Имя или порода не заполнены", Toast.LENGTH_SHORT).show(); return; } Cat cat = new Cat(); cat.setName(name); cat.setBreed(breed); mCatDao.insert(cat); updateCats(); } }
И адаптера:
package info.javaway.greendaotutorial; import android.app.Activity; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.List; import info.javaway.greendaotutorial.model.Cat; public class CatAdapter extends RecyclerView.Adapter<CatAdapter.CatHolder> { private List<Cat> mCats; private LayoutInflater mInflater; private AppCompatActivity mAppCompatActivity; public CatAdapter(List<Cat> cats, LayoutInflater inflater, AppCompatActivity activity) { mCats = cats; mInflater = inflater; mAppCompatActivity = activity; } @Override public CatHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = mInflater.inflate(R.layout.cat_item, parent, false); return new CatHolder(view); } @Override public void onBindViewHolder(CatHolder holder, int position) { Cat cat = mCats.get(position); holder.bindCat(cat); } @Override public int getItemCount() { return mCats.size(); } public void setCats(List<Cat> cats) { mCats = cats; notifyDataSetChanged(); } class CatHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private TextView mCatName; private TextView mCatBreed; private Cat cat; public CatHolder(View itemView) { super(itemView); mCatName = (TextView) itemView.findViewById(R.id.cat_name); mCatBreed = (TextView) itemView.findViewById(R.id.cat_breed); itemView.setOnClickListener(this); } public void bindCat(Cat cat) { this.cat = cat; mCatName.setText(cat.getName()); mCatBreed.setText(cat.getBreed()); } @Override public void onClick(View view) { Cat cat = mCats.get(getLayoutPosition()); Long catId = cat.getId(); ((MainActivity)mAppCompatActivity).getCatDao().deleteByKey(catId); ((MainActivity)mAppCompatActivity).updateCats(); } } }
Ну вот, теперь наши коты не разбегаются при выходе из приложения, а аккуратно упаковываются в базу данных.
По-моему библиотека заслуживает внимания, она мало весит и показывает хорошие результаты в скорости. Если нужно, могу дополнить пример ещё несколькими таблицами с разными связями, код из примера можете взять на Гитхабе. Всем добра!
P.s. Если есть неудержимое желание поддержать Javaway и котиков, можете сделать это тут =)
Spasibo bolshoe ochen’ udobniy primer