
전형적인 안드로이드 앱 권장 아키텍쳐
저장소
Room 데이터베이스
데이터 접근 객체
엔티티
SQLite 데이터베이스

Room 기반 데이터베이스 저장소를 제공하는 방법
@Entity(tableName = "customers")
class Customer {
@PrimaryKey(autoGenerate = true)
@NotNull
@ColumInfo(name = "customerId")
var id: Int = 0
@ColumInfo(name = "customerName")
var name : String? = null
var address: String? = null
constructor(){}
constructor(id : Int, name : String, address : String){
this.id = id
this.name = name
this.address = address
}
constructor(name : String, address : String){
this.name = name
this.address = address
}
}
//데이터베이스에 저장하지 않을 때
@Ignore
var MyString : String? = null
@Dao
interface CustomerDao{
@Query("SELECT * FROM customers")
fun getAllCustomers() : LiveData<List<Customer>>
}
@Insert
fun addCustomer(Customer customer)
@Delete
fun deleteCustomers(Customer... customer)
@Update
fun updateCustomers(Customer... customers)
@Database(entities = [(Customer::class)], version = 1)
abstract class CustomerRoomDatabse : RoomDatabase() {
abstract fun customerDao() : CustomerDao companion object{
private var INSTANCE : CustomerRoomDatabse? = null
fun getInstance(context : Context) : CustomerRoomDatabse{
synchronized(this){
var instance = INSTANCE
if(instacne == null){
instance = Room.databaseBuilder(
context.applicationContext,
CustomerRoomDatabse::class.java,
"customer_database"
).fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
class CustomerRepository(private val customerDao : CustomerDao){
private val coroutineScope = CoroutineScope(Dispatchers.Main)
//...
fun insertCustomer(customer: Customer){
coroutineScope.launch(Dispatchers.IO){
customerDao.insertCustomer(customer)
}
}
fun deleteCustomer(name : String){
coroutineScope.launch(Dispatchers.IO){
customerDao.deleteCustomer(name)
}
}
}
//파일 젖아소 기반 데이터베이스를 생성
instance = Room.databaseBuilder(
context.applicationContext,
CustomerRoomDatabase::class.java,
"customer_database").fallbackToDestructiveMigration()
.build()
//인메모리 데이터베이스를 생성한다.
instance = Room.inMemoryDatabaseBuilder(
context.applicationContext,
CustomerRoomDatabase::class.java)
.fallbackToDestructivMigration()
.build()
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//추가하기
id 'kotlin-kapt'
}
//...
dependencies {
//room과 livedata 환경 빌드
implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.room:room-ktx:2.5.2'
implementation 'androidx.compose.runtime:runtime-livedata:1.4.3'
annotationProcessor "androidx.room:room-compiler:2.5.2"
}
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.jetbrains.annotations.NotNull
@Entity(tableName = "products")
class Product {
@PrimaryKey(autoGenerate = true)
@NotNull
@ColumnInfo(name = "productId")
var id : Int = 0
@ColumnInfo(name = "productName")
var productName : String = ""
var quantity : Int = 0
constructor()
constructor(productName : String, quantity : Int){
this.productName = productName
this.quantity = quantity
}
}
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface ProductDao {
@Insert
fun insertProduct(product: Product)
@Query("SELECT * FROM products WHERE productName = :name")
fun findProduct(name : String) : List<Product>
@Query("DELETE FROM products WHERE productName = :name")
fun deleteProduct(name : String)
@Query("SELECT * FROM products")
fun getAllProducts() : LiveData<List<Product>>
}
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Product::class], version = 1)
abstract class ProductRoomDatabase : RoomDatabase() {
abstract fun productDao(): ProductDao
companion object {
private var INSTANCE: ProductRoomDatabase? = null
fun getInstance(context: Context): ProductRoomDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
ProductRoomDatabase::class.java,
"product_database"
).fallbackToDestructiveMigration().build()
INSTANCE = instance
}
return instance
}
}
}
}
//코루틴
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
class ProductRepository(private val productDao: ProductDao) {
val allProducts : LiveData<List<Product>> = productDao.getAllProducts()
val searchResult = MutableLiveData<List<Product>>()
private val coroutineScope = CoroutineScope(Dispatchers.Main)
fun insertProduct(newprodect : Product){
coroutineScope.launch(Dispatchers.IO) {
productDao.insertProduct(newprodect)
}
}
fun deleteProduct(name : String){
coroutineScope.launch(Dispatchers.IO){
productDao.deleteProduct(name)
}
}
fun fileProduct(name : String){
coroutineScope.launch(Dispatchers.Main) {
searchResult.value = asyncFind(name).await()
}
}
private fun asyncFind(name : String) : Deferred<List<Product>?> = coroutineScope.async(Dispatchers.IO){
return@async productDao.findProduct(name)
}
}
//ViewModel 추가하기
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class MainViewModel(application : Application) : ViewModel() {
val allProducts : LiveData<List<Product>>
private val repository : ProductRepository
val searchResults : MutableLiveData<List<Product>>
init {
val productDb = ProductRoomDatabase.getInstance(application)
val productDao = productDb.productDao()
repository = ProductRepository(productDao)
allProducts = repository.allProducts
searchResults = repository.searchResult
}
fun insertProduct(product: Product){
repository.insertProduct(product)
}
fun findProduct(name : String){
repository.findProduct(name)
}
fun deleteProduct(name : String){
repository.deleteProduct(name)
}
}
@Composable
fun TitleRow(head1: String, head2: String, head3: String) {
Row(
modifier = Modifier
.background(MaterialTheme.colors.primary)
.fillMaxWidth()
.padding(5.dp)
) {
Text(head1, color = Color.White, modifier = Modifier.weight(0.1f))
Text(head2, color = Color.White, modifier = Modifier.weight(0.2f))
Text(head3, color = Color.White, modifier = Modifier.weight(0.2f))
}
}
@Composable
fun ProductRow(id: Int, name: String, quantity: Int) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(5.dp)
) {
Text(text = id.toString(), modifier = Modifier.weight(0.1f))
Text(text = name, modifier = Modifier.weight(0.2f))
Text(text = quantity.toString(), modifier = Modifier.weight(0.2f))
}
}
@Composable
fun CustomTextField(
title: String,
textState: String,
onTextChange: (String) -> Unit,
keyboardType: KeyboardType
) {
OutlinedTextField(
value = textState,
onValueChange = onTextChange,
keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
singleLine = true,
label = {
Text(
text = title
)
},
modifier = Modifier.padding(10.dp),
textStyle = TextStyle(fontWeight = FontWeight.Bold, fontSize = 30.sp)
)
}
class MainViewModel(application : Application) : ViewModel() {
val allProducts : LiveData<List<Product>>
private val repository : ProductRepository
val searchResults : MutableLiveData<List<Product>>
init {
val productDb = ProductRoomDatabase.getInstance(application)
val productDao = productDb.productDao()
repository = ProductRepository(productDao)
allProducts = repository.allProducts
searchResults = repository.searchResult
}
fun insertProduct(product: Product){
repository.insertProduct(product)
}
fun findProduct(name : String){
repository.findProduct(name)
}
fun deleteProduct(name : String){
repository.deleteProduct(name)
}
}
import android.annotation.SuppressLint
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.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.chap43_roomdatabase.ui.theme.Chap43_RoomDatabaseTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Chap43_RoomDatabaseTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val owner = LocalViewModelStoreOwner.current
owner?.let {
val viewModel : MainViewModel = viewModel(
it,
"MainViewModel",
MainViewModelFactory(LocalContext.current.applicationContext as Application)
)
ScreenSetup(viewModel)
}
}
}
}
}
}
class MainViewModelFactory(val application : Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(application) as T
}
}
@Composable
fun ScreenSetup(viewModel : MainViewModel) {
val allProducts by viewModel.allProducts.observeAsState(listOf())
val searchResults by viewModel.searchResults.observeAsState(listOf())
MainScreen(allProducts, searchResults, viewModel)
}
@SuppressLint("SuspiciousIndentation")
@Composable
fun MainScreen(allProducts : List<Product>, searchResults : List<Product>, viewModel: MainViewModel) {
var productName by remember {
mutableStateOf("")
}
var productQuantity by remember {
mutableStateOf("")
}
var searching by remember {
mutableStateOf(false)
}
val onProductTextChange = { text : String ->
productName = text
}
val onQuantityTextChange = { text : String ->
productQuantity = text
}
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
CustomTextField(
title = "Product Name",
textState = productName,
onTextChange = onProductTextChange,
keyboardType = KeyboardType.Text
)
CustomTextField(
title = "Quantity",
textState = productQuantity,
onTextChange = onQuantityTextChange,
keyboardType = KeyboardType.Number
)
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
Button(onClick = {
if(productQuantity.isNotEmpty())
viewModel.insertProduct(Product(productName, productQuantity.toInt()))
searching = false
}) {
Text(text = "Add")
}
Button(onClick = {
searching = true
viewModel.findProduct(productName)
}) {
Text(text = "Search")
}
Button(onClick = {
searching = false
viewModel.deleteProduct(productName)
}) {
Text(text = "Delete")
}
Button(onClick = {
searching = false
productName = ""
productQuantity = ""
}) {
Text(text = "Clear")
}
}
LazyColumn(
Modifier
.fillMaxWidth()
.padding(10.dp)){
val list = if(searching) searchResults else allProducts
item{
TitleRow(head1 = "ID", head2 = "Product", head3 = "Quantity")
}
items(list){ product ->
ProductRow(id = product.id, name = product.productName, quantity = product.quantity)
}
}
}
}
@Composable
fun TitleRow(head1: String, head2: String, head3: String) {
Row(
modifier = Modifier
.background(MaterialTheme.colors.primary)
.fillMaxWidth()
.padding(5.dp)
) {
Text(head1, color = Color.White, modifier = Modifier.weight(0.1f))
Text(head2, color = Color.White, modifier = Modifier.weight(0.2f))
Text(head3, color = Color.White, modifier = Modifier.weight(0.2f))
}
}
@Composable
fun ProductRow(id: Int, name: String, quantity: Int) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(5.dp)
) {
Text(text = id.toString(), modifier = Modifier.weight(0.1f))
Text(text = name, modifier = Modifier.weight(0.2f))
Text(text = quantity.toString(), modifier = Modifier.weight(0.2f))
}
}
@Composable
fun CustomTextField(
title: String,
textState: String,
onTextChange: (String) -> Unit,
keyboardType: KeyboardType
) {
OutlinedTextField(
value = textState,
onValueChange = onTextChange,
keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
singleLine = true,
label = {
Text(
text = title
)
},
modifier = Modifier.padding(10.dp),
textStyle = TextStyle(fontWeight = FontWeight.Bold, fontSize = 30.sp)
)
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
Chap43_RoomDatabaseTheme {
}
}
Room Database를 이용하여 앱을 종료하고 다시 실행해도 저장된 데이터가 있다.
This version (1.2.0) of the Compose Compiler requires Kotlin version 1.7.0 but you appear to be using Kotlin version 1.9.0-RC which is not known to be compatible.
위와같은 오류가 발생했다. 이유는 1.7.0 버전을 사용해야하는데 1.9.0-RC 버전을 사용하고 있다며 오류가 발생한다.
[해결방법]
먼저 오류 버전 1.2.0은 낮은 버전으로 되어있다. 공식 문서에 보면 해당 날짜 기준 1.4.3 사용을 하는 것으로 되어있다.
https://developer.android.com/jetpack/androidx/releases/compose-compiler?hl=ko#kts
그래서 해당 부분을 1.4.3으로 변경했다.

앱 모듈 그래들 파일 안에 있는 코드 부분을 1.4.3으로 변경한다.
그런데도 아마 똑같은 오류가 발생할 것이다. 이유는 1.9.0-RC를 사용해서 그럴 것이다. 내용을 보면 변경되어 1.8.10으로 바꾸라고 오류 내용이 나올 것이다.
그러면 이제 프로젝트 그래들파일에 가서 코틀린 버전을 1.8.10으로 바꿔주면 된다.

프로젝트 그래들 파일

위와같이 does not exist라는 오류가 발생했다.
처음에는 최신버전이라 책에 있는 버전으로 바꾸었으나 똑같은 오류가 발생했다.
그래서 Room 라이브러리 공식 문서를 확인을 했는데 하나 의존성 주입을 빼먹었다.
공식 문서 사이트 : https://developer.android.com/topic/libraries/architecture/room?hl=ko
위 공식 문서 사이트 중에
// To use Kotlin annotation processing tool (kapt)
kapt("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
ksp("androidx.room:room-compiler:$room_version")
본인은 kapt를 등록했으므로 첫 번째 부분을 추가하면 된다. room_version은 의존성 주입한 버전과 맞춰 하면 된다.