Пример работы с SQLite и Room

Последнее обновление: 16.04.2024

Рассмотрим примитивный практически пример работы с базой данных SQLite через библиотеку Room с использованием ViewModel. Финальный проект будет выглядеть следующим образом:

работа с SQLite через Room в приложении на Jetpack Compose и Kotlin в Android

Пусть в файле User.kt будет располагаться класс User - сущность, с которой мы будем работать:

package com.example.helloapp

import androidx.annotation.NonNull
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "users")
class User {
    @PrimaryKey(autoGenerate = true)
    @NonNull
    @ColumnInfo(name = "userId")
    var id: Int = 0
    @ColumnInfo(name = "userName")
    var name: String=""
    var age: Int = 0

    constructor() {}

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

Класс User будет сопоставляться с таблицей "users".

В файле UserDao.kt располагается интерфейс UserDao, который определяет методы для взаимодействия с базой данных:

package com.example.helloapp

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getUsers(): LiveData<List<User>>

    @Insert
    fun addUser(user: User)

    @Query("DELETE FROM users WHERE userId = :id")
    fun deleteUser(id:Int)
}

Здесь определены три метода. Метод getUsers() будет выполнять SELECT-запрос и возвращает список всех объектов User из базы данных в виде объекта LiveData<List<User>>. Метод addUser() принимает добавляемый объект User и выполняет INSERT-запрос. И метод deleteUser() удаляет объект User из базы данных по id.

В файле UserRoomDatabase.kt располагается класс базы данных Room:

package com.example.helloapp

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

@Database(entities = [(User::class)], version = 1)
abstract class UserRoomDatabase: RoomDatabase() {

    abstract fun userDao(): UserDao

    // реализуем синглтон
    companion object {
        private var INSTANCE: UserRoomDatabase? = null
        fun getInstance(context: Context): UserRoomDatabase {

            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        UserRoomDatabase::class.java,
                        "usersdb"
                    ).fallbackToDestructiveMigration().build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }
}

В данном случае UserRoomDatabase определяет объект-синглтон, поскольку в приложении база данных Room должна существовать в одном в единственном виде. В качестве имени базы данных устанавливается "usersdb".

В файле UserRepository.kt расположен класс репозитория, через который приложение будет взаимодействовать с базой данных через объект UserDao:

package com.example.helloapp

import androidx.lifecycle.LiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class UserRepository(private val userDao: UserDao) {
    private val coroutineScope = CoroutineScope(Dispatchers.Main)

    val userList: LiveData<List<User>> = userDao.getUsers()

    fun addUser(User: User) {
        coroutineScope.launch(Dispatchers.IO) {
            userDao.addUser(User)
        }
    }

    fun deleteUser(id:Int) {
        coroutineScope.launch(Dispatchers.IO) {
            userDao.deleteUser(id)
        }
    }
}

Класс репозитория определяет ряд методов, которые обращаются к методам объекта UserDao, который передается через конструктор. Для этого используются корутины, чтобы избежать выполнения операций с базой данных в основном потоке.

Отдельно стоит сказать про свойство userList, которое хранит список всех объектов из БД. Для его получения вызывается метод userDao.getUsers(). Причем репозиторию необходимо вызвать этот метод один раз при инициализации и сохранить результат в объекте LiveData, который может наблюдаться ViewModel и, в свою очередь, объектом Activity. После этого каждый раз, когда в таблице базы данных будет происходить изменение, компонент, который отслеживает изменения, получит уведомление об изменениях и будет перекомпонован с использованием обновленного списка userList.

В файле UserViewModel.kt расположен класс UserViewModel - модель представления, который будет выполнять роль посредника между репозиторием и графическим интерфейсом:

package com.example.helloapp

import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel

class UserViewModel(application: Application) : ViewModel() {

    val userList: LiveData<List<User>>
    private val repository: UserRepository
    var userName by mutableStateOf("")
    var userAge by mutableStateOf(0)

    init {
        val userDb = UserRoomDatabase.getInstance(application)
        val userDao = userDb.userDao()
        repository = UserRepository(userDao)
        userList = repository.userList
    }
    fun changeName(value: String){
        userName = value
    }
    fun changeAge(value: String){
        userAge = value.toIntOrNull()?:userAge
    }
    fun addUser() {
        repository.addUser(User(userName, userAge))
    }
    fun deleteUser(id: Int) {
        repository.deleteUser(id)
    }
}

Класс ViewModel принимает экземпляр контекста приложения, который представлен классом Android Context и который используется в коде приложения для получения доступа к ресурсам приложения во время выполнения. Кроме того, в контексте приложения можно вызывать широкий спектр методов для сбора информации и внесения изменений в среду приложения. В нашем случае контекст приложения необходим при создании базы данных.

ViewModel определяет ряд переменных.

val userList: LiveData<List<User>>
private val repository: UserRepository
var userName by mutableStateOf("")
var userAge by mutableStateOf(0)

userList прелставляет список пользователей, полученный из базы данных. для взаимодействия с БД определяется переменная репозитория - repository. И для управления вводом новых данных для имени и возраста пользователя определяются две переменных состояния - userName и userAge.

Блок инициализатора создает базу данных, которая используется для создания объекта UserDao. Затем мы используем UserDao для инициализации репозитория и получения данных в userList:

init {
    val userDb = UserRoomDatabase.getInstance(application)
    val userDao = userDb.userDao()
    repository = UserRepository(userDao)
    userList = repository.userList
}

И также UserViewModel определяет ряд методов, которые будут вызываться при изменении ввода в текстовом поле или при нажатии на кнопку.

И наконец определим сам интерфейс в MainActivity.kt:

package com.example.helloapp

import android.app.Application
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel

import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel


class UserViewModelFactory(val application: Application) :
    ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return UserViewModel(application) as T
    }
}

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val owner = LocalViewModelStoreOwner.current

            owner?.let {
                val viewModel: UserViewModel = viewModel(
                    it,
                    "UserViewModel",
                    UserViewModelFactory(LocalContext.current.applicationContext as Application)
                )
                Main(viewModel)
            }
        }
    }
}


@Composable
fun Main(vm: UserViewModel = viewModel()) {
    val userList by vm.userList.observeAsState(listOf())
    Column {
        OutlinedTextField(vm.userName, modifier= Modifier.padding(8.dp), label = { Text("Name") }, onValueChange = {vm.changeName(it)})
        OutlinedTextField(vm.userAge.toString(), modifier= Modifier.padding(8.dp), label = { Text("Age") },
            onValueChange = {vm.changeAge(it)},
            keyboardOptions = KeyboardOptions(keyboardType= KeyboardType.Number)
        )
        Button({ vm.addUser() }, Modifier.padding(8.dp)) {Text("Add", fontSize = 22.sp)}
        UserList(users = userList, delete = {vm.deleteUser(it)})
    }
}
@Composable
fun UserList(users:List<User>, delete:(Int)->Unit) {
    LazyColumn(Modifier.fillMaxWidth()) {
        item{ UserTitleRow()}
        items(users) {u -> UserRow(u, {delete(u.id)})  }
    }
}
@Composable
fun UserRow(user:User, delete:(Int)->Unit) {
    Row(Modifier .fillMaxWidth().padding(5.dp)) {
        Text(user.id.toString(), Modifier.weight(0.1f), fontSize = 22.sp)
        Text(user.name, Modifier.weight(0.2f), fontSize = 22.sp)
        Text(user.age.toString(), Modifier.weight(0.2f), fontSize = 22.sp)
        Text("Delete", Modifier.weight(0.2f).clickable { delete(user.id) }, color=Color(0xFF6650a4), fontSize = 22.sp)
    }
}
@Composable
fun UserTitleRow() {
    Row(Modifier.background(Color.LightGray).fillMaxWidth().padding(5.dp)) {
        Text("Id", color = Color.White,modifier = Modifier.weight(0.1f), fontSize = 22.sp)
        Text("Name", color = Color.White,modifier = Modifier.weight(0.2f), fontSize = 22.sp)
        Text("Age", color = Color.White, modifier = Modifier.weight(0.2f), fontSize = 22.sp)
        Spacer(Modifier.weight(0.2f))
    }
}

Для создания модели представления UserViewModel ей необходимо передать ссылку на объект Application (контекст приложения). Стандартная функция viewModel(), которая обычно применяется для создания моделей представления, не позволяет этого сделать. Поэтому вместо использования viewModel() мы создаем свой собственный класс ViewModelProvider.Factory, предназначенный для передачи ссылки на объект Application и возврата объекта UserViewModel.

class UserViewModelFactory(val application: Application) :
    ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return UserViewModel(application) as T
    }
}

Кроме фабрики типа ViewModelProvider.Factory функция viewModel() также требует ссылки на текущий объект ViewModelStoreOwner. Этот объект представляет своего рода контейнер, в котором хранятся все активные в данный момент модели представления ViewModel. Используяя название ViewModel (в нашем случае "UserViewModel"), можно получить нужную модель представления:

setContent {
    val owner = LocalViewModelStoreOwner.current

    owner?.let {
    val viewModel: UserViewModel = viewModel(
        it,
        "UserViewModel",
        UserViewModelFactory(LocalContext.current.applicationContext as Application)
        )
    Main(viewModel)
    }
}

Здесь вначале получаем текущий объект ViewModelStoreOwner, используя свойство LocalViewModelStoreOwner.current. Провереяем полученный объект на null и вызываем функцию viewModel(), в которую передаем объект ViewModelStoreOwner (it), идентифицирующую строку модели представления ("UserViewModel") и фабрику модели представления UserViewModelFactory (которой передается ссылка на контекст приложения).

Получив объект ViewModel, передаем его в компонент Main, который определяет интерфейс приложения:

@Composable
fun Main(vm: UserViewModel = viewModel()) {
    val userList by vm.userList.observeAsState(listOf())
    Column {
        OutlinedTextField(vm.userName, modifier= Modifier.padding(8.dp), label = { Text("Name") }, onValueChange = {vm.changeName(it)})
        OutlinedTextField(vm.userAge.toString(), modifier= Modifier.padding(8.dp), label = { Text("Age") },
            onValueChange = {vm.changeAge(it)},
            keyboardOptions = KeyboardOptions(keyboardType= KeyboardType.Number)
        )
        Button({ vm.addUser() }, Modifier.padding(8.dp)) {Text("Add", fontSize = 22.sp)}
        UserList(users = userList, delete = {vm.deleteUser(it)})
    }
}

Компонент получает из модули представления список пользователей в переменную userList. Причем благодаря получению с помощью функции vm.userList.observeAsState() компонент будет отслеживать все изменения в списке, и если в список будет добавлены новые объекты или из него будут удалены ранее существовавшие, то компонент будет обновлен, чтобы отразить эти изменения.

Внутри компонента определен простейший интерфейс. Прежде всего это два текстовых поля для ввода данных для имени и возраста пользователя, при изменении которых срабатывают функции changeName и changeAge из UserViewModel. Также определена кнопка, при нажатии на которую вызывается функция addUser, которая добавляет нового пользователя.

И также вызывается кастомный компонент UserList, который выводит список пользователей:

@Composable
fun UserList(users:List<User>, delete:(Int)->Unit) {
    LazyColumn(Modifier.fillMaxWidth()) {
        item{ UserTitleRow()}
        items(users) {u -> UserRow(u, {delete(u.id)})  }
    }
}

Для вывода списка применяется LazyColumn, который для отображения заголовка использует кастомный компонент UserTitleRow, а для вывода данных каждого пользователя - UserRow.

@Composable
fun UserRow(user:User, delete:(Int)->Unit) {
    Row(Modifier .fillMaxWidth().padding(5.dp)) {
        Text(user.id.toString(), Modifier.weight(0.1f), fontSize = 22.sp)
        Text(user.name, Modifier.weight(0.2f), fontSize = 22.sp)
        Text(user.age.toString(), Modifier.weight(0.2f), fontSize = 22.sp)
        Text("Delete", Modifier.weight(0.2f).clickable { delete(user.id) }, color=Color(0xFF6650a4), fontSize = 22.sp)
    }
}

Здесь выводятся все свойства объекта User, а последнbq компонент Text выстпает в качестве кнопки, по нажатию на которую срабаывает метод deleteUser в UserViewModel.

Запустим приложение и добавим объект в базу данных:

база данных SQLite и Room в приложении на Jetpack Compose и Kotlin в Android

Причем данные автоматически отобразяться в списке объектов. Аналогичным образом можно добавить больше объектов или удалять их из базы данных:

операции с бозой данных SQLite в Room в приложении на Jetpack Compose и Kotlin в Android
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850