Android Room база данных

База данных Room — это часть компонентов архитектуры Android, которая обеспечивает уровень абстракции над SQLite, что дает более надежный доступ к базе данных, сохраняя при этом всю мощь SQLite. Room — это библиотека Android  Jetpack.

Преимущества Room

  • Проверка SQL-запросов во время компиляции. Каждый @Query и @Entity проверяется во время компиляции.
  • Использование аннотаций для сокращения стандартного кода.
  • Легко интегрируется с другими компонентами архитектуры, такими как LiveData, RxJava, Coroutines.

Архитектура Room

Основные компоненты Room

1. @Entity

Entity  Позволяет сделать представление таблицы и столбцов очень простым. Всего лишь нужно аннотировать  @Entity  к классу, и имя класса становится именем таблицы, а элементы данных становятся именами столбцов. Класс @Entity представляет сущность в таблице.

@Entity(tableName = "user")
data class Users(@PrimaryKey(autoGenerate = true)var userId: Int? = null,
                 val userName: String, var location: String, val email: String)

2. @Dao

@Dao — объект доступа к данным, или другими словами – это интерфейс в который мы помещаем все наши SQL-запросы. Теперь нам не нужно писать запросы целиком. Можно сделать метод и аннотировать с помощью конкретных аннотаций, таких как:

@Insertиспользуется для вставки записи в базу данных Room
@Delete используется для удаления записи из базы данных Room
@Update – используется для обновления записи в базе данных комнат
@Query – используется для запросов на считывание данных из базы данных

@Dao
interface UserDao {
    @Insert
    fun insertUser(users: Users)

    @Query("Select * from user")
    fun gelAllUsers(): List<Users>

    @Update
    fun updateUser(users: Users)

    @Delete
    fun deleteUser(users: Users)
}

3. @Database

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

@Database(entities = [Users::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao() : UserDao

Пример приложения базы данных Room. Самый простой вариант, без Coroutines и без ViewModel

Я собираюсь создать простенькое приложения для управления пользователями, используя базу данных Room в kotlin. В приложении я добавил функции для вставки, удаления, обновления и считывания всех пользователей. Получится приложение заготовка. Не забывайте package при копировании заменять на свой.

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

1. Добавим зависимости

Нам нужно добавить зависимости для базы данных Room в наш  файл build.gradle.

plugins {
    ...
    id 'kotlin-kapt'
    id 'kotlin-android-extensions'
}
    ...
buildTypes {
    ...
    }
    // Это ViewBinding если кто не знает что это почитайте в Google
    buildFeatures{
        viewBinding true
    }
    ...
dependencies {
    ...
    def roomVersion = '2.4.2'
    implementation "androidx.room:room-ktx:$roomVersion"
    kapt "androidx.room:room-compiler:$roomVersion"
    androidTestImplementation "androidx.room:room-testing:$roomVersion"

    implementation 'com.google.code.gson:gson:2.9.0'
}

2. Создадим класс модели

Cоздадим модель для таблицы Room, для каждого класса добавим аннотацию @Entity .

  • Аннотируйте класс  @Entity и используйте  tableName, чтобы задать имя таблицы.
  • Задайте первичный ключ, добавив в  нужные поля аннотацию @primaryKey  — в нашем случае это userId.
  • Задайте имя столбцов для полей класса, используя  @ColumnInfo. Вы можете пропустить этот шаг, так как @ColumnInfo указывать не обязательно.
  • Если подходят несколько конструкторов, добавьте  @Ignore аннотацию, чтобы указать Room, какой из них следует использовать, а какой нет.

Файл Model.kt

package ru.jandroid.roomdatabase_my2

import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable

@Entity(tableName = "user")
data class Users(@PrimaryKey(autoGenerate = true)var userId: Int? = null,
                 val userName: String, var location: String, val email: String):Serializable

3. Создайте DAO (объекты доступа к данным)

DAO отвечают за определение методов доступа к базе данных.

Файл интерфейс UserDao

package ru.jandroid.roomdatabase_my2

import androidx.room.*

@Dao
interface UserDao {

    @Insert
    fun insertUser(users: Users)

    @Query("Select * from user")
    fun gelAllUsers(): List<Users>

    @Update
    fun updateUser(users: Users)

    @Delete
    fun deleteUser(users: Users)
}

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

4. Преобразователи типов

Преобразователи типов используются, когда мы объявляем свойство, которое Room и SQL не знают, как сериализовать. Давайте добавим в наш проект возможность сериализации типа данных List<String>. Хотя эта конвертация данных сейчас нам не нужна, но в бедующем много вероятно, что она понадобится. Например если вы захотите положить в ячейку базы данных коллекцию строк или массив строк, то эта функция преобразует ваш список в строку JSON и поместит в ячейку базы данных, а когда вы захотите получить список из ячейки обратно, то JSON конвертируется обратно в список строк.

Файл Converters

package ru.jandroid.roomdatabase_my2

import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

class Converters {

    @TypeConverter
    fun fromString(value: String): List<String> {
        val listType = object : TypeToken<List<String>>() {

        }.type
        return Gson().fromJson(value, listType)
    }

    @TypeConverter
    fun fromArrayList(list: List<String>): String {
        val gson = Gson()
        return gson.toJson(list)
    }
}

5. Создадим базу данных

Теперь все готово для создания базы данных. Мы можем создать базу данных Room, расширив RoomDatabase.

Файл AppDatabase

package ru.jandroid.roomdatabase_my2

import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import android.content.Context

@Database(entities = [Users::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao() : UserDao

    companion object {
        private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase? {
            if (INSTANCE == null) {
                synchronized(AppDatabase::class) {
                    INSTANCE = Room.databaseBuilder(context.applicationContext,
                        AppDatabase::class.java, "user.db").allowMainThreadQueries()
                        .build()
                }
            }
            return INSTANCE
        }

        fun destroyInstance() {
            INSTANCE = null
        }
    }
}

На что следует обратить внимание:

  • Это абстрактный класс, который должен расширяться от RoomDatabase.
  • Он должен быть аннотирован с помощью @Database, он получает список сущностей со всеми классами, составляющими базу данных (все эти классы должны быть аннотированы с помощью @Entity). Мы также должны предоставить версию базы данных.
  • Мы должны объявить абстрактную функцию для каждой из сущностей, включенных в аннотацию @Database, эта функция должна возвращать соответствующий DAO (класс, аннотированный @Dao).
  • Наконец, мы объявляем companion object для получения статического доступа к методу getAppDataBase, который дает нам единственный экземпляр базы данных.

6. Операции CRUD в базе данных Room

Теперь база данных Room готова к операциям CRUD (копировать, читать, редактировать, удалять).

Файл UserRepository

package ru.jandroid.roomdatabase_my2

import android.content.Context
import android.os.AsyncTask

class UserRepository(context: Context) {

    var db: UserDao = AppDatabase.getInstance(context)?.userDao()!!


    //Fetch All the Users
    fun getAllUsers(): List<Users> {
        return db.gelAllUsers()
    }

    // Insert new user
    fun insertUser(users: Users) {
        insertAsyncTask(db).execute(users)
    }

    // update user
    fun updateUser(users: Users) {
        db.updateUser(users)
    }

    // Delete user
    fun deleteUser(users: Users) {
        db.deleteUser(users)
    }

    private class insertAsyncTask internal constructor(private val usersDao: UserDao) :
        AsyncTask<Users, Void, Void>() {

        override fun doInBackground(vararg params: Users): Void? {
            usersDao.insertUser(params[0])
            return null
        }
    }
}

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

Лучше для этого подходят Coroutines, но это в другой раз.

7. Добавим файлы макета экранов

В макете у вас будет требовать иконку для удаления элемента списка, добавьте ее самостоятельно. Если не будет каких либо цветов, добавьте их самостоятельно.

Файл activity_add_user.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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">

    <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:ems="10"
            android:id="@+id/etUserName"
            android:layout_marginEnd="8dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:hint="Username"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"/>

    <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:inputType="text"
            android:ems="10"
            android:id="@+id/etEmail"
            android:layout_marginEnd="8dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:hint="Email ID"
            app:layout_constraintTop_toBottomOf="@+id/etUserName"/>

    <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:ems="10"
            android:id="@+id/etLocation"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/etEmail"
            android:layout_marginEnd="8dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:hint="Location"/>

    <Button
            android:text="Save User"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/buttonSaveUser"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/etLocation"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

Файл layout_user_list.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
                                             xmlns:app="http://schemas.android.com/apk/res-auto"
                                             android:layout_width="match_parent"
                                             android:layout_height="wrap_content">

    <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp">


        <TextView
            android:id="@+id/textUserName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="TextView"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium"
            app:layout_constraintEnd_toStartOf="@+id/imgDelete"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/textLocation"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="TextView"
            app:layout_constraintEnd_toStartOf="@+id/imgDelete"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/textEmail" />

        <TextView
            android:id="@+id/textEmail"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="TextView"
            android:layout_marginTop="8dp"
            app:layout_constraintEnd_toStartOf="@+id/imgDelete"
            app:layout_constraintLeft_toLeftOf="@id/textUserName"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/textUserName" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_marginTop="8dp"
            android:background="@color/purple_700"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/textLocation" />

        <ImageView
            android:id="@+id/imgDelete"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:background="@drawable/ic_baseline_close_24"
            android:backgroundTint="@color/design_default_color_error"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

Файл activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:clickable="true"
            app:srcCompat="@drawable/ic_add_black_24dp"
            android:id="@+id/floatingActionButton"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"/>

    <androidx.recyclerview.widget.RecyclerView
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            android:id="@+id/recyclerViewUsers"/>
</androidx.constraintlayout.widget.ConstraintLayout>

8. Добавим адаптер для RecyclerView

Файл UserListAdapter.kt

package ru.jandroid.roomdatabase_my2

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import ru.jandroid.roomdatabase_my2.databinding.LayoutUserListBinding

class UserListAdapter : RecyclerView.Adapter<MyViewHolder>() {

    var userList = mutableListOf<Users>()

    var clickListener: ListClickListener<Users>? = null

    fun setUsers(users: List<Users>) {
        this.userList = users.toMutableList()
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.layout_user_list, parent, false)
        return MyViewHolder(view)
    }

    override fun getItemCount(): Int {
        return userList.size
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {

        val user = userList[position]
        holder.binding.run {
            textLocation.text = user.location
            textUserName.text = user.userName
            textEmail.text = user.email
            layout.setOnClickListener {
                clickListener?.onClick(user, position)
            }

            imgDelete.setOnClickListener {
                clickListener?.onDelete(user)
            }
        }
    }

    fun setOnItemClick(listClickListener: ListClickListener<Users>) {
        this.clickListener = listClickListener
    }
}

class MyViewHolder(view: View): RecyclerView.ViewHolder(view) {
    val binding = LayoutUserListBinding.bind(view)
}


interface ListClickListener<T> {
    fun onClick(data: T, position: Int)
    fun onDelete(user: T)
}

9. Создадим активити для добавления элементов в базу данных Room

Не забудьте добавить это активити в AndroidManifest.xml

Файл AddUserActivity

package ru.jandroid.roomdatabase_my2

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.widget.Toast
import ru.jandroid.roomdatabase_my2.databinding.ActivityAddUserBinding

class AddUserActivity : AppCompatActivity() {
    lateinit var binding:ActivityAddUserBinding

    var user: Users? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityAddUserBinding.inflate(layoutInflater)
        setContentView(binding.root)

        user = intent.getSerializableExtra("user") as Users?

        user?.let {
            binding.etUserName.setText(it.userName)
            binding.etLocation.setText(it.location)
            binding.etEmail.setText(it.email)
        } ?: kotlin.run {

        }

        val repo = UserRepository(this)

        binding.buttonSaveUser.setOnClickListener {

            if (binding.etUserName.text.isNotEmpty() && binding.etEmail.text.isNotEmpty() && binding.etLocation.text.isNotEmpty()) {

                user?.let {
                    val user = Users(userId = it.userId,
                        userName = binding.etUserName.text.toString(),
                        location = binding.etLocation.text.toString(),
                        email = binding.etEmail.text.toString()
                    )
                    repo.updateUser(user)
                } ?: kotlin.run {
                    val user = Users(
                        userName = binding.etUserName.text.toString(),
                        location = binding.etLocation.text.toString(),
                        email = binding.etEmail.text.toString()
                    )
                    repo.insertUser(user)
                }

            } else {
                Toast.makeText(this, "Invalid Input", Toast.LENGTH_SHORT).show()
            }
            finish()
        }
    }

}

10. Создадим главное активити

Файл MainActivity

package ru.jandroid.roomdatabase_my2

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.GridLayoutManager
import ru.jandroid.roomdatabase_my2.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    lateinit var binding:ActivityMainBinding
    lateinit var adapter: UserListAdapter
    val repo:UserRepository by lazy { UserRepository(this) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        init()
        listeners()
    }

    // Слушатели
    fun listeners(){
        adapter.setOnItemClick(object : ListClickListener<Users>{
            override fun onClick(data: Users, position: Int) {
                val intent = Intent(this@MainActivity,AddUserActivity::class.java)
                intent.putExtra("user", data)
                startActivity(intent)
            }

            override fun onDelete(user: Users) {
                repo.deleteUser(user)
                fetchUsers()
            }
        })


        binding.floatingActionButton.setOnClickListener {
            val intent = Intent(this@MainActivity,AddUserActivity::class.java)
            startActivity(intent)
        }
    }

    // Инициализация
    fun init(){
        adapter = UserListAdapter()
        binding.recyclerViewUsers.layoutManager = GridLayoutManager(this@MainActivity,1)
        binding.recyclerViewUsers.adapter = adapter

    }

    override fun onResume() {
        super.onResume()
        fetchUsers()
    }

    // Наполнение RecyclerView
    fun fetchUsers() {
        val allUsers = repo.getAllUsers()
        adapter.setUsers(allUsers)
    }
}

Заключение

На этом все. Приложение должно заработать. В следующих уроках сделаем базу данных на карутинах. Спасибо за внимание.

Поделись с друзьями:
Если вам понравилась статья, подписывайтесь на наши социальные сети.

Оставьте комментарий

один × 5 =