Библиотека «Room» для начинающего Android-разработчика

от автора

Здравствуй, дорогой читатель. Каждый Android-разработчик сталкивался (или столкнётся во время своей профессиональной карьеры) с задачей, в которой необходимо хранить большое количество изменяемых данных. В данной статье будет разобрана библиотека от Google — Room.

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

Статья предназначена для новичков, не знакомых с данной библиотекой, но, желательно, имеющих базовые знаниях о SQLite, Kotlin Coroutines, Kotlin Flow, MVVM.

Готовый практический пример, который будет разобран в статье.
Готовый практический пример, который будет разобран в статье.

Все материалы и исходный код можно найти здесь.

Теоретическая часть

Для хранения каких-либо данных, Android-разработчику предоставлены следующие способы: Files, SharedPreferences, SQLite, Resources/Assets. Но какой способ выбрать? Для ответа на данный вопрос можно воспользоваться алгоритмом, изображенном на рисунке ниже.

Алгоритм выбора правильного способа хранения данных в Android.
Алгоритм выбора правильного способа хранения данных в Android.

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

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

Один из способов для работы с SQLite в Android — это, встроенный в Android SDK, SQL API. Данное API позволяет работать с базой данных, но, по-моему мнению, данная технология далеко не простая, а в некоторых моментах даже сложная. В данной статье мы не будем разбирать данную технологию, а сразу перейдем к библиотеке «Room».

Room — это библиотека, представленная на Google I/O в 2017 году. Данная библиотека работает с базой данный SQLite и выполняет большую часть работы за Вас. Все что необходимо разработчику — это «объяснить» библиотеке как выглядят данные, их структуру и способы взаимодействия с помощью специальных аннотаций:

  • @Database — аннотация для объявления базы данных.

  • @Entity — аннотация для объявления сущности базы данных.

  • @Dao — аннотация для объявления интерфейса, который будет заниматься манипулированием данными базы данных.

  • @PrimaryKey — аннотация для объявления первичного ключа сущности.

  • @ColumnInfo — аннотация для настроек конкретного столбца сущности.

  • @Query — аннотация, которая позволяет выполнить SQL-запрос в методах DAO-интерфейса.

  • @Insert — аннотация, которая позволяет выполнить вставку в таблицу базы данных.

  • @Update — аннотация, которая позволяет выполнить обновление некоторых строк в таблице базы данных.

  • @Delete — аннотация, которая позволяет выполнить удаление некоторых строк в таблице базы данных.

  • @Transaction — аннотация, которая помечает метод в DAO-интерфейсе как транзакция.

Это далеко не все аннотации, которые предоставляет «Room», но являющиеся основными. Более подробно аннотации будут рассмотрены в практическом примере.

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

Практическая часть

В качестве не сложного примера, создадим приложение, которое будет «имитировать» создание и отображение статистических данных какой-то игры (например, судоку). Приложение будет состоять из двух экранов: первый — заполнение и отправка данных в базу; второй — список со всеми данными из базы.

Статистические данные будут состоять из следующих компонентов: результат игры (победа / поражение), уровень сложности (легкая, сложная и т.д.), количество ошибок, количество набранных очков.

Важное уточнение. В данной статье не будут приведены листинги кода с версткой xml-файлов и всех классов приложения. Полностью готовый проект можно найти здесь.

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

dependencies {      ...      implementation 'androidx.room:room-runtime:2.5.0' // Библиотека "Room"     kapt "androidx.room:room-compiler:2.5.0" // Кодогенератор     implementation 'androidx.room:room-ktx:2.5.0' // Дополнительно для Kotlin Coroutines, Kotlin Flows }
plugins {      ...      id 'kotlin-kapt' }
android {      ...      defaultConfig {          ...          kapt {             arguments {arg("room.schemaLocation", "$projectDir/schemas")}         }     } }

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

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

  • таблица «difficulty_levels», в которой будут хранится все доступные уровни сложности;

  • таблица «results», в которой будут хранится все доступные результаты игры;

  • таблица «statistic», в которой будут хранится все статистические данные.

Схематично база данных будет выглядеть следующим образом:

Схема базы данных.
Схема базы данных.

SQL-скрипт для такой базы данных выглядел бы следующим образом:

CREATE TABLE difficulty_levels(     id INTEGER PRIMARY KEY,     difficulty_name TEXT );  CREATE TABLE results (     id INTEGER PRIMARY KEY,     result_name TEXT );  CREATE TABLE statistic (     id INTEGER PRIMARY KEY,     result_id INTEGER,     difficult_id INTEGER,     mistakes INTEGER,     points INTEGER,     FOREIGN KEY (result_id) REFERENCES results(id),     FOREIGN KEY (difficult_id) REFERENCES difficulty_levels(id) );

Примечание: никогда не храните секретные данные (например, пароли) в открытом виде. Всегда хэшируйте их!

После того, как была разобрана схема базы данных, необходимо перейти к созданию сущностей. Создадим data-class DifficultyLevelsDbEntity, который будет описывать таблицу «difficulty_levels»:

@Entity(tableName = "difficulty_levels") data class DifficultyLevelsDbEntity(     @PrimaryKey val id: Long,     @ColumnInfo(name = "difficulty_name") val difficultyName: String )

В данном случае мы пометили класс аннотацией @Entity, в которой переопределили свойство tableName — данное свойство задаёт имя таблицы. В случае, если бы свойство не было бы определенно, то таблица назвалась аналогично названию класса, т.е. DifficultyLevelsDbEntity.

Также поля класса были помечены аннотациями @PrimaryKey и @ColumnInfo. Первая аннотация помечает поле класса, как первичный ключ, а вторая задаёт название столбца отличное от названии переменной. У аннотации @ColumnInfo есть и другие свойства, например значение по умолчанию, более подробно про свойства данной аннотации можно узнать здесь.

Аналогично создадим класс ReultsDbEntity:

@Entity(tableName = "results") data class ResultsDbEntity(     @PrimaryKey val id: Long,     @ColumnInfo(name = "result_name") val resultName: String )

Теперь перейдем к созданию более сложной сущности — StatisticDbEntity:

@Entity(     tableName = "statistic",     indices = [Index("id")],     foreignKeys = [         ForeignKey(             entity = ResultsDbEntity::class,             parentColumns = ["id"],             childColumns = ["result_id"]         ),         ForeignKey(             entity = DifficultyLevelsDbEntity::class,             parentColumns = ["id"],             childColumns = ["difficult_id"]         )     ] ) data class StatisticDbEntity(     @PrimaryKey(autoGenerate = true) val id: Long,     @ColumnInfo(name = "result_id") val resultId: Long,     @ColumnInfo(name = "difficult_id") val difficultId: Long,     val mistakes: Long,     val points: Long )

Аннотации в свойствах data-class’а очень похожи на те, что были созданы ранее. Единственное отличие — в @PrimaryKey было определено свойство autoGenerate = true. Данное свойство «объясняет» библиотеке, что при вставке нового объекта в таблицу, необходимо сгенерировать индекс самостоятельно. Например, в таблице находится пять элементов, при вставке нового элемента поле id автоматически станет равно шести.

Также в аннотации @Entity было определенно больше свойств. Свойство indices «объясняет» библиотеки по каком полю производить индексацию, в данном случае — id.

Свойство foreignKeys объявляет составные ключи. В данном случае составных ключа два — result_id и difficult_id. В объекте ForeignKey указываются сущность-родитель (entity), столбец-родитель (parentColumns) и столбец-ребенок (childColumns).

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

@Dao interface StatisticDao {  } 

Когда сущности были созданы, dao-интерфейс объявлен необходимо создать абстрактный класс AppDatabase, который будет описывать базу данных:

@Database(     version = 1,     entities = [         DifficultyLevelsDbEntity::class,         ResultsDbEntity::class,         StatisticDbEntity::class     ] ) abstract class AppDatabase : RoomDatabase() {      abstract fun getStatisticDao(): StatisticDao  }

Данный класс помечен аннотацией @Database, в которой необходимо обязательно описать два свойства: entities и version. Первое свойство принимает все сущности, которые были описаны выше, а второе свойство задаёт версию базы данных.

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

В самом классе создан абстрактный метод, который возвращает dao-интерфейс.

Так же создадим tupple-класс StatisticInfoTuple, который будет использоваться при «вытягивании» статистических данных из таблицы. Данный класс очень похож на StatisticDbEntity, но поля, которые хранят результат и уровень сложности, уже имеют тип String, так как там будут хранится значения результата и уровня сложности.

data class StatisticInfoTuple(     val id: Long,     @ColumnInfo(name = "result_name") val result: String,     @ColumnInfo(name = "difficulty_name") val difficult: String,     val mistakes: Long,     val points: Lon

После всех проделанных действий необходимо перейти к реализации dao-интерфейса. В данном интерфейсе создадим три метода, которые будут вставлять новые статистические данные, удалять данные по уникальному значению (id) и получать список всех данных из таблицы statistic:

    @Insert(entity = StatisticDbEntity::class)     fun insertNewStatisticData(statistic: StatisticDbEntity)      @Query("SELECT statistic.id, result_name, difficulty_name, mistakes, points FROM statistic\n" +             "INNER JOIN results ON statistic.result_id = results.id\n" +             "INNER JOIN difficulty_levels ON statistic.difficult_id = difficulty_levels.id;")     fun getAllStatisticData(): List<StatisticInfoTuple>      @Query("DELETE FROM statistic WHERE id = :statisticId")     fun deleteStatisticDataById(statisticId: Long) 

Метод insertNewStatisticData принимает объект класса StatisticDbEntitty — объект, который необходимо вставить. Также данный метод помечен аннотацией @Insert, в которой определено свойство entity, благодаря которому происходит вставка в нужную таблицу.

Методы getAllStatisticData и deleteStatisticDataById помечены аннотацией @Query, которая принимает строку с SQL-запросом. Именно благодаря данному запросу выполняется получение всех элементов или удаление какого-то конкретного элемента.

И последнее, что необходимо сделать с базой данной — создать ее. Для этого выполним следующее:

object Dependencies {      private lateinit var applicationContext: Context      fun init(context: Context) {         applicationContext = context     }      private val appDatabase: AppDatabase by lazy {         Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database.db")             .createFromAsset("room_article.db")             .build()     } }

В данном примере создается база данных appDatabase с помощью специального билдера: Room.databaseBuilder, который принимает контекст, класс, содержащий описание нашей базы данных, и название самой базы.

Так же у билдера вызван метод createFromAssets, данный метод заполняет базу данных приготовленными значениями. Т.е., если необходимо, чтобы при инициализации база данных хранила в себе какие-либо значения (например, уровни сложности и доступные результаты), нужно создать отдельно базу данных с помощью сторонних программ, таких как DB Browser for SQLite, заполнить ее и сохранить ее в папку assets приложения.

После того, как все манипуляции с базой данных были реализованы, необходимо создать data-класс Statistic, который будет использоваться во всем приложении. В данном классе создан метод toStatisticDbEntity, конвертирующий данный класс в сущность:

data class Statistic(     val resultId: Long,     val difficultId: Long,     val mistakes: Long,     val points: Long ) {      fun toStatisticDbEntity(): StatisticDbEntity = StatisticDbEntity(         id = 0,         resultId = resultId,         difficultId = difficultId,         mistakes = mistakes,         points = points       ) }

Теперь необходимо создать репозиторий, который будет обращаться к dao-интерфейсу и манипулировать данными базы данных:

class StatisticRepository(private val statisticDao: StatisticDao) {      suspend fun insertNewStatisticData(statisticDbEntity: StatisticDbEntity) {         withContext(Dispatchers.IO) {             statisticDao.insertNewStatisticData(statisticDbEntity)         }     }      suspend fun getAllStatisticData(): List<StatisticInfoTuple> {         return withContext(Dispatchers.IO) {             return@withContext statisticDao.getAllStatisticData()         }     }      suspend fun removeStatisticDataById(id: Long) {         withContext(Dispatchers.IO) {             statisticDao.deleteStatisticDataById(id)         }     }   }

В данном репозитории три метода, которые вставляют новые данные, получают всю статистику и удаляют какой-то элемент по идентификатору. Все эти методы являются suspend-функциями, т.к. будут вызываться из корутин. Так же следует изменить Dispatcher (withContext), т.к. обращаться к базе данных из основного потока нельзя.

Все эти методы вызываются в корутинах, запущенных во ViewModel, например вставка нового значения:

fun insertNewStatisticDataInDatabase(mistakes: Long, points: Long) {         viewModelScope.launch {             val newStatistic = Statistic(currentResult, currentDifficultyLevel, mistakes, points)             statisticRepository.insertNewStatisticData(newStatistic.toStatisticDbEntity())         } }

Весь рабочий код Вы можете найти здесь.

Рекомендованные источники

В данной статье были разобраны основы библиотеки. Естественно, это далеко не все, что позволяет делать «Room». Если Вы хотите сильнее углубиться в эту тему изучите следующие источники, которые могут помочь Вам:


ссылка на оригинал статьи https://habr.com/ru/post/713518/


Комментарии

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

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