Содержание
База данных 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)
}
}
Заключение
На этом все. Приложение должно заработать. В следующих уроках сделаем базу данных на карутинах. Спасибо за внимание.