
컴포즈에서 룸데이터베이스 사용
모듈 단위의 build.gradle.kts에서
상단의 plugins에는 kotlin("kapt") 추가
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
kotlin("kapt")
}
하단의 dependencies에는 3개 추가
dependencies {
...
implementation("androidx.room:room-runtime:2.6.1")
annotationProcessor("androidx.room:room-compiler:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
}


package kr.co.lion.jetpackexample.db
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "MemoTable")
data class MemoEntity(
@PrimaryKey(autoGenerate = true)
var memoIdx:Int = 0,
var memoSubject:String = "",
var memoText:String = ""
)

package kr.co.lion.jetpackexample.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface MemoDao {
// 저장
@Insert
fun insertMemoData(memoEntity: MemoEntity)
// 메모 내용 전체 가져오기
@Query("""
select memoIdx, memoSubject, memoText
from memotable
order by memoIdx desc
""")
fun selectMemoDataAll() : List<MemoEntity>
// 메모 내용 하나만 가져오기
@Query("""
select memoIdx, memoSubject, memoText
from memotable
where memoIdx = :memoIdx
""")
fun selectMemoDataOne(memoIdx:Int) : MemoEntity
}

package kr.co.lion.jetpackexample.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [MemoEntity::class], version = 1)
abstract class MemoDatabase: RoomDatabase() {
// Dao
abstract fun memoDao(): MemoDao
companion object{
// 데이터 베이스 객체를 담을 정적 변수
var memoDatabase: MemoDatabase? = null
// 데이터 베이스 객체를 생성하는 메서드
@Synchronized
fun getInstance(context: Context): MemoDatabase?{
// 데이터 베이스 객체가 생성된 적이 없을 경우
if(memoDatabase == null){
synchronized(MemoDatabase::class){
// 데이터 베이스 객체를 생성한다.
// 마지막에 데이터 베이스 파일명을 넣어준다.
memoDatabase = Room.databaseBuilder(
context.applicationContext,
MemoDatabase::class.java,
"memo.db"
).build()
}
}
return memoDatabase
}
}
}
만들어놓은 Entity대로 테이블이 만들어진다
제목, 내용 입력 요소와 연결되어 있는 데이터 관리 요소를 메서드 상단으로 올린다.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InputScreen(navHostController: NavHostController) {
// 제목 입력 요소와 연결되어 있는 데이터 관리 요소
val subjectTextState = remember {
mutableStateOf("")
}
// 내용 입력 요소와 연결되어 있는 데이터 관리 요소
val contentTextState = remember {
mutableStateOf("")
}
우측 상단 메뉴의 onClick 부분을 수정한다.
Screen은 액티비티나 프래그먼트가 아닌 Composable함수이므로 context가 필요할 경우 LocalContext.current를 사용하면 된다.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InputScreen(navHostController: NavHostController) {
// context
val context = LocalContext.current
...
// 우측 상단 메뉴
actions = {
IconButton(
onClick = {
// 데이터 베이스 객체를 가져온다.
val memoDatabase = MemoDatabase.getInstance(context)
// 데이터를 담는다.
val memoEntity = MemoEntity(memoSubject = subjectTextState.value, memoText = contentTextState.value)
// 저장
CoroutineScope(Dispatchers.Main).launch {
async(Dispatchers.IO) {
memoDatabase?.memoDao()?.insertMemoData(memoEntity)
}
navHostController.popBackStack()
}
}
젯팩 컴포즈에서 코루틴은 화면과 관련된 작업을 할 수 있기 때문에
컴포즈에서 코루틴을 사용해야하는 경우, 눈에 보이는 UI요소(Scaffold)가 전부 불러와진 후에 작업을 해야 한다.
만약 UI요소가 전부 그려지기도 전에 코루틴 작업을 시도하려고 한다면 아래와 같이 코드에 에러표시가 뜬다.

앞의 InputScreen에서 작업한 내용의 경우, onClick안에 코루틴을 작성하는 것은 버튼 UI가 만들어진 후에 사용자가 버튼을 누른다는 것이 확정된 부분이기 때문에 에러가 발생하지 않는 것이다.
이 때문에 Composable에 대한 라이프사이클 운영을 지원하고 있고 이를 이용하면 화면요소들이 모두 불러와진 이후에 자동으로 원하는 작업을 수행하도록 할 수 있다.
// Compose 라이프사이클을 관리하는 Composable
@Composable
fun rememberLifeCycleEvent(lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): Lifecycle.Event{
// 라이프 사이클 값을 담고 있을 리멤버 객체
var state by remember{
// 모든 라이프 사이클 상태값을 담을 리멤버 객체로 생성한다.
mutableStateOf(Lifecycle.Event.ON_ANY)
}
// 라이프 사이클 관리 객체와 state 리멤버 변수를 연결해준다.
DisposableEffect(lifecycleOwner) {
// 라이프사이클 감시자
// 라이프 사이클이 변경되면 지정된 메서드가 호출된다.
val observer = LifecycleEventObserver{ _, event ->
// 새로운 라이프사이클 상태를 리멤버 프로퍼티에 넣어준다.
// 그 후 리멤버 프로퍼티를 사용한 모든 코드들이 다시 동작을 하게 된다.
state = event
}
// 감시자를 라이프사이클 관리 객체에 연결해준다.
lifecycleOwner.lifecycle.addObserver(observer)
// Composable이 종료되면 감시자도 자동 해지되게 해준다.
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
return state
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(navHostController: NavHostController) {
val context = LocalContext.current
// 데이터 베이스 객체를 가져온다.
val memoDatabase = MemoDatabase.getInstance(context)
// 라이프 사이클을 관리하는 객체를 생성하여 반환하는 Compose는 이 파일 제일 하단에 있다.
// 라이프 사이클을 관리하는 객체를 가져온다.
val lifecycleEvent = rememberLifeCycleEvent()
// 리스트뷰 구성을 위해 사용할 리멤버 변수
val memoList = remember {
mutableStateOf(listOf<MemoEntity>())
}
// 라이프 사이클이 변경될 때 자동으로 동작하는 부분을 만들어준다.
LaunchedEffect(lifecycleEvent) {
// 라이프사이클이 ON_RESUME 상태일때
if(lifecycleEvent == Lifecycle.Event.ON_START || lifecycleEvent == Lifecycle.Event.ON_RESUME){
// 데이터를 가져온다.
val job1 = async (Dispatchers.IO){
// 데이터를 가져온다.
memoDatabase?.memoDao()?.selectMemoDataAll()
}
memoList.value = job1.await() as List<MemoEntity>
}
}
LazyColumn{
// 리스트
// 100개의 항목을 가진 리스트를 생성한다.
// LazyColumn 안에 있기 때문에 보이지 않는 항목들은 생성이 대기
// 사라진 항목들은 새롭게 나타난 항목들을 위해 재사용된다.
// it에는 몇번째 항목인지 값이 들어온다
items(memoList.value.size){
// 항목 하나의 모양을 구성한다.
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxSize()
// 항목을 눌렀을 때
.clickable {
// 결과 화면이 보이도록 한다.
navHostController.navigate(ScreenName.OutputScreen.name)
}
) {
Text(text = memoList.value[it].memoSubject)
}
Divider()
}
}

결과화면으로 넘어갈 때 메모 번호를 받도록 구성해준다.
// 결과 화면
composable(
// 결과를 보여주는 화면은 메모 번호가 필요하기 때문에 메모 번호를 받도록 구성해준다.
// 화면의 이름/값 : 값 부분으로 전달되는 값이 memoIdx라는 이름으로 추출할 수 있다.
route = "${ScreenName.OutputScreen.name}/{memoIdx}",
...
...
// 보여줄 화면으로 값을 전달한다.
// arguments에 담은 객체는 다음에 보여줄 화면 Composable로 전달된다.
arguments = listOf(
// memoIdx라는 이름으로 전달되는 값을 담아준다.
navArgument("memoIdx"){
// 전달할 값의 타입
type = NavType.IntType
}
)
){
// ResultScreen이 구성되도록 호출한다.
// navHostController와 데이터를 추출할 수 있는 객체를 전달한다.
ResultScreen(navHostController = navController, it)
}
ResultScreen에서 전달한 memoIdx값을 사용할 수 있게된다.
항목을 눌렀을 때 결과 화면이 보여지는 부분을 수정한다.
items(memoList.value.size){
// 항목 하나의 모양을 구성한다.
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxSize()
// 항목을 눌렀을 때
.clickable {
// 결과 화면이 보이도록 한다.
navHostController.navigate("${ScreenName.OutputScreen.name}/${memoList.value[it].memoIdx}")
}
) {
Text(text = memoList.value[it].memoSubject)
}
Divider()
}
MainScreen에서 작성했던 라이프사이클을 관리하는 Composable을 사용한다.
또는 MainScreen에서 작성하지말고 MainActivity에서 작성하거나 따로 빼도 된다.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
// 전달받은 데이터를 추출하기 위한 객체를 받을 매개 변수를 전달한다.
fun ResultScreen(navHostController: NavHostController, navBackStackEntry: NavBackStackEntry) {
// 전달 받은 데이터를 추출한다.
var memoIdx = navBackStackEntry.arguments?.getInt("memoIdx")
// 출력할 데이터를 가지고 있는 리멤버 프로퍼티
val memoData = remember{
mutableStateOf(MemoEntity())
}
// 라이프 사이클을 관리하는 객체를 생성하여 반환하는 Compose는 MainScreen.kt 파일 제일 하단에 있다.
// 라이프 사이클을 관리하는 객체를 가져온다.
val lifecycleEvent = rememberLifeCycleEvent()
// 라이프 사이클이 변경될 때 자동으로 동작하는 부분을 만들어준다.
LaunchedEffect(lifecycleEvent) {
// 라이프사이클이 ON_RESUME 상태일때
if(lifecycleEvent == Lifecycle.Event.ON_START || lifecycleEvent == Lifecycle.Event.ON_RESUME){
// 데이터를 가져온다.
val job1 = async (Dispatchers.IO){
// 데이터를 가져온다.
memoDatabase?.memoDao()?.selectMemoDataOne(memoIdx!!)
}
memoData.value = job1.await() as MemoEntity
}
}
불러온 메모 데이터를 출력한다.
// 위에서 아래방향으로 배치하는 레이아웃
Column(
// fillMaxSize : 화면의 크기를 단말기 전체 화면으로 설정한다.
// padding : 여백. Scaffold의 it 안에는 상단 툴바 만큼의 여백이 설정되어 있다.
// background : 배경 색상
modifier = Modifier
.fillMaxSize()
.padding(it)
.background(Color.White)
) {
Text(text = "제목 : ${memoData.value.memoSubject}")
// 여백
Spacer(modifier = Modifier.padding(top = 10.dp))
Text(text = "내용 : ${memoData.value.memoText}")
}
