Рассмотрим примитивный практически пример работы с базой данных SQLite через библиотеку Room с использованием ViewModel. Финальный проект будет выглядеть следующим образом:
Пусть в файле 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.
Запустим приложение и добавим объект в базу данных:
Причем данные автоматически отобразяться в списке объектов. Аналогичным образом можно добавить больше объектов или удалять их из базы данных: