본문 바로가기
Android/Jetpack Compose App

JETPACK COMPOSE: 노트 앱 만들기 - 2, ViewModel, Android ROOM, Dependency Injection(DI), Hilt & Dagger, Coroutine

by 개발자J의일상 2022. 4. 1.
반응형

지난 시간 리뷰

JETPACK COMPOSE: 노트 앱 만들기 - 1, ViewModel, Android ROOM, Dependency Injection(DI), Hilt & Dagger, Coroutine

 

JETPACK COMPOSE: 노트 앱 만들기 - 1, ViewModel, Android ROOM, Dependency Injection(DI), Hilt & Dagger, Coroutine

지난 시간 리뷰 JETPACK COMPOSE: 영화 앱 만들기 - 2, Navigation Component, Scaffold, LazyColumn, Passing Data Between Screens, Coil JETPACK COMPOSE: 영화 앱 만들기 - 2, Navigation Component, Scaffold..

mypark.tistory.com

 

지난 시간에 노트 앱 만들기에서는 Note를 작성하고 저장하고 삭제하는 과정과 Composable 함수를 구현하여 UI를 만드는 방법에 대해 살펴보았습니다.

 

MainActivity에서 notes를 아래와 같이 mutableStateListOf<Note>로 생성을 해주었습니다. 

val notes = remember {
    mutableStateListOf<Note>()
}

 

그리고 이 notes를 NotesScreenNoteRow에 각각 매개변수 형태로 전달하여 Note에 접근하였습니다.

Android Jetpack Compose: The Comprehensive Bootcamp [2022]

이러한 방식은 List<Note>를 접근하려면 매번 함수의 매개변수로 넘겨줘야 하는 불편함과 

다른 Activity나 Fragment가 있을 때 이 List<Note>에 접근할 수 없습니다.

Android Jetpack Compose: The Comprehensive Bootcamp [2022]

Android에서 제공하는 ViewModel을 사용하면 이러한 단점을 해결할 수 있습니다.

Lifecycle에 상관없이 데이터를 관리할 수 있는 장점이 있습니다.

또한 모든 함수에서 ViewModel을 통해 List<Note>에 접근할 수 있습니다.

 

ViewModel을 사용하려면 몇가지 작업이 필요합니다.

https://developer.android.com/jetpack/compose/libraries

 

Compose 및 기타 라이브러리  |  Jetpack Compose  |  Android Developers

Compose 및 기타 라이브러리 Compose에서는 자주 이용하는 라이브러리를 사용할 수 있습니다. 이 섹션에서는 몇 가지 가장 유용한 라이브러리를 통합하는 방법을 설명합니다. Activity 활동에서 Compose

developer.android.com

 

build.gradle에 아래 implementation을 추가해줘야 합니다.

dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1"
}

 

우리는 ViewModel을 상속하는 NoteViewModel 클래스를 생성해주고

noteList라는 새로운 mutableStateListOf<Note>를 private로 만들어 줍니다.

 

생성자 호출 시 NotesDataSource에 저장된 note들을 불러와서 noteList에 넣어주고, addNote와 removeNote를 지난시간에 람다로 구현했던 것들을 그대로 구현합니다. 

getAllNotes() 호출 시 저장된 noteList를 반환해줍니다.

package com.example.jetnote.screen

import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import com.example.jetnote.data.NotesDataSource
import com.example.jetnote.model.Note

class NoteViewModel : ViewModel() {
    private var noteList = mutableStateListOf<Note>()

    init {
        noteList.addAll(NotesDataSource().loadNotes())
    }
    fun addNote(note: Note) {
        noteList.add(note)
    }

    fun removeNote(note: Note) {
        noteList.remove(note)
    }

    fun getAllNotes(): List<Note> {
        return noteList
    }
}

 

이제 MainActivity 부분을 수정해 봅시다.

noteViewModel을 아래와 같이 선언하고 by viewModels()를 해줍니다.

by viewModels()를 사용하여 초기화를 하면 해당 viewModel이 초기화 되는 Activity나 Fragment의 Lifecycle에 종속되게 됩니다. 

by는 Kotlin의 delegate property입니다.

onAddNote와 onRemoveNote는 noteViewModel에 있는 함수로 대체를 합니다.

@ExperimentalComposeUiApi
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetnoteTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    val noteViewModel: NoteViewModel by viewModels()
                    NotesApp(noteViewModel)
                }
            }
        }
    }
}

@ExperimentalComposeUiApi
@Composable
fun NotesApp(noteViewModel: NoteViewModel = viewModel()) {
    val notesList = noteViewModel.getAllNotes()
    NoteScreen(notes = notesList,
        onAddNote = noteViewModel::addNote,
        onRemoveNote = noteViewModel::removeNote
    )
}

 

developer.android.com에서는 아래와 같이 사용하도록 권장하고 있습니다.

class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val model: MyViewModel by viewModels()
        model.getUsers().observe(this, Observer<List<User>>{ users ->
            // update UI
        })
    }
}

ViewModel이 activity의 onCreate() 메서드가 호출될 때 생성됩니다.

재 생성된 activity들에서도 첫번째 activity에서 생성된 동일한 MyViewModel 인스턴스를 받을 수 있게 됩니다.

 

여기까지 하면 앱을 종료하고 나면 데이터가 모두 사라지게됩니다.

우리가 원하는건 앱을 종료하여도 데이터가 남아있는 것입니다.

 

이를 구현하기 위해서는 어떻게 해야될까요?

 

ROOM을 사용하면 됩니다!

ROOM은 Jetpack에서 제공하는 기능 중 하나입니다.

 

ROOM의 정의는 아래와 같습니다.

Room 지속성 라이브러리는 SQLite에 추상화 레이어를 제공하여 SQLite를 완벽히 활용하면서 더 견고한 데이터베이스 액세스를 가능하게 합니다.

https://developer.android.com/jetpack/androidx/releases/room

 

Room  |  Android 개발자  |  Android Developers

Room Room 지속성 라이브러리는 SQLite에 추상화 레이어를 제공하여 SQLite를 완벽히 활용하면서 더 견고한 데이터베이스 액세스를 가능하게 합니다. 최근 업데이트 안정적인 버전 출시 후보 베타 버

developer.android.com

 

우리는 이 ROOM을 통해서 쉽고 안전하게 note들을 우리의 device에 저장할 수 있습니다.

 

우리는 DI (Dependency Injection)이라는 개념도 살펴보려고합니다.

 

Dependency는 객체가 다른 객체의 함수를 사용하려고할 때 생깁니다.

 

DI라는 컨셉은 android 개발에서 굉장히 많이 쓰이고 매우 추천되는 방식입니다.

 

아래 예제를 들어서 설명해보겠습니다.

 

Engine은 Car에 반드시 필요합니다.

 

간단하게 Car와 Engine 클래스를 만들어 보았습니다.

fun main() {
    val car = Car()
    car.moveCar()
}

class Engine() {
    fun start() {
        println("Engine is started!")
    }
}

class Car() {
    val engine = Engine()
    fun moveCar() {
        println("moving Car! ${engine.start()}")
    }
}

 

이 코드에는 문제가 있는데요.

 

Car를 생성할 때 마다 engine이 생성되는 문제가 있습니다.

이것은 single responsibility principle (SRP)를 위반합니다. 각 클래스는 하나의 책임만 담당해야 합니다.

Car에는 다른 Engine 클래스가 내부에 있기 때문에 좋은 코드는 아닙니다.

또한 Car에는 여러 Engine이 있을 수 있습니다.

1600cc Engine이 있을 수도있고 3300cc Engine이 있을 수도 있습니다.

Engine이 바뀔 때 val을 계속해서 바꿔주거나 여러 engine을 val로 선언해야 합니다.

 

아래 코드는 더이상 SRP를 위반하지 않습니다.

engine을 main에서 생성하여 Car의 생성자로 넘기기 때문에 더이상 Car 클래스 안에 engine이 선언된 것이 아니기 때문입니다.

이를 Engine을 Car에 주입(inject)한다라고합니다.

fun main() {
    val engine = Engine()
    val car = Car(engine)
    car.moveCar()
}

class Engine() {
    fun start() {
        println("Engine is started!")
    }
}

class Car(val engine: Engine) {
    //val engine = Engine() 
    fun moveCar() {
        println("moving Car! ${engine.start()}")
    }
}

 

아래와 같이 우리는 다른 engine도 추가하여 car클래스에 inject시킬 수 있습니다.

Car class는 이것을 가지고 해당 클래스의 함수를 호출할 수 있게 됩니다.

기존과 같이 하려면 val에 새로운 engine class를 생성해야 하지만 지금은 그냥 생성자에 추가해주면 됩니다. 

fun main() {
    val engine = Engine()
    val threethousandengine = ThreeThousandCCEngine()
    val car = Car(engine, threethousandengine)
    car.engine.start()
    car.threethousandengine.start()
    car.moveCar()
}

class Engine() {
    fun start() {
        println("Engine is started!")
    }
}
class ThreeThousandCCEngine() {
    fun start() {
        println("ThreeThousandCCEngine is started")
    }
}

class Car(val engine: Engine, val threethousandengine: ThreeThousandCCEngine) {
    //val engine = Engine() 
    fun moveCar() {
        println("moving Car! ${engine.start()}")
    }
}

여기까지 DI에 대해 간단한 예제를 살펴보았습니다.

 

Hilt and Dagger를 통해 DI를 구현할 수 있습니다. 

여러 자료들이 있기 때문에 검색을 해보면 더 자세한 설명을 얻을 수 있습니다.

처음 접하기에는 굉장히 복잡하기 때문에 천천히 살펴보려고 합니다. 

 

앞으로는 이를 사용하여 DI를 구현하고자 합니다. 

DI를 직접 구현해도 상관이 없지만 app 단위에서 이것을 관리하고 사용하는 것은 굉장히 어려운 일입니다.

그래서 이를 해결하기 위해 Hilt and Dagger가 등장했습니다.

 

DI의 장점으로는 아래와 같이 3가지를 크게 꼽을 수 있습니다.

1. 코드 재사용 가능(Reusability of code)

2. 리팩터링 편의성(Ease of refactoring)

3. 테스트 편의성(Ease of testing)

 

Hilt and Dagger는 202년 6월 Google에서 공식적으로 발표한 Android를 위한 DI 라이브러리입니다.

Android Framework에서 표준으로 사용되는 DI 컴포넌트와 scope를 기본적으로 제공, 초기에 DI 환경을 구축하는 비용을 크게 절감 시키는 것이 주요한 목표입니다.

 

Hilt and Dagger를 사용하기 위해서는 아래 설정이 필요합니다.

https://developer.android.com/training/dependency-injection/hilt-android#groovy

 

Hilt를 사용한 종속 항목 삽입  |  Android 개발자  |  Android Developers

Hilt를 사용한 종속 항목 삽입 Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. 수동 종속 항목 삽입을 실행하려면 모든 클래스

developer.android.com

 

제 세팅 아래 첨부합니다. 혹시 막히시는분은 아래 코드를 참고하세요~

buildscript {
    ext {
        compose_version = '1.0.4'
        hilt_version = '2.41'
    }
    dependencies {
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '7.1.1' apply false
    id 'com.android.library' version '7.1.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.5.31' apply false
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.example.jetnote"
        minSdk 26
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {
    //Hilt-Dagger
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-compiler:$hilt_version"

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
    implementation 'androidx.activity:activity-compose:1.4.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1"
}

 

먼저 해야할 일은 @HiltAndroidApp 어노테이션을 추가해주는 것입니다.

@HiltAndroidApp 어노테이션은 컴파일 타임에 표준 컴포넌트 빌딩에 필요한 클래스들을 초기화 합니다.

Hilt 셋업을 위해서 반드시 필요한 과정입니다.

 

우리는 NoteApplication class를 생성하고 Application()을 상속하는 @HiltAndroidApp을 아래와 같이 만들어줍니다.

package com.example.jetnote

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class NoteApplication : Application() {

}

 

android:name

애플리케이션에 관해 구현되는 Application 서브클래스의 정규화된 이름입니다. 애플리케이션 프로세스가 시작되면 애플리케이션의 구성요소보다 먼저 이 클래스가 인스턴스화됩니다.
이 서브클래스는 선택사항이며 대부분의 애플리케이션에 필요가 없습니다. 서브클래스가 없으면 Android는 기본 Application 클래스의 인스턴스를 사용합니다.

 

AndroidManifest.xml에 아까 생성한 class NoteApplication을 아래와 같이 등록해줍니다.

android:name=".NoteApplication"

Application level에서 이제 이 class를 사용할 수 있게 됩니다.

//app/manifests/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.jetnote">

    <application
        android:name=".NoteApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Jetnote">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Jetnote">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

이제 Hilt Module을 생성해 줘야 합니다.

먼저 @Module 어노테이션을 작성해야합니다.

Hilt는 표준으로 제공하는 component들이 존재하기 때문에 @InstallIn 어노테이션을 사용하여 표준 component에 모듈들을 설치할 수 있습니다. 

 

아래는 새로운 package di를 만들고 그 안에 AppModule object를 생성하였습니다. 

object에 대해 모르시는 분은 아래 링크를 참고해주세요

Kotlin Objects 정리

 

Kotlin Objects 정리

object 키워드는 대략 클래스처럼 보이는 것을 정의합니다. 그러나 객체의 인스턴스를 만들 수는 없습니다. 객체는 하나만 있습니다. 이를 때때로 Singleton pattern(싱글톤 패턴)이라고 합니다. object(

mypark.tistory.com

 

아래 코드는 결국 AppModule을 SingletonComponent에 install하고, SingletonComponent에서 제공해주는 것들을 사용할 수 있는 것입니다.

package com.example.jetnote.di

import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
}

Injection에 사용되는 Component

@AndroidEntryPoint와 같은 Hilt API를 사용하여 Android 클래스를 삽입할 때 표준 Hilt component가 injector로 사용됩니다.

injector로 사용되는 component는 해당 Android 클래스에 표시되는 바인딩을 결정합니다.

사용된 구성 요소는 아래 표에 나와 있습니다.

Component Injector for
SingletonComponent Application
ViewModelComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent View with @WithFragmentBindings
ServiceComponent Service

 

Component 수명

Component의 수명은 두 가지 중요한 방식으로 바인딩의 수명과 관련되기 때문에 중요합니다.

 

1. Component가 생성된 시점부터 파기된 시점까지의 범위 내 바인딩의 라이프 타임을 제한합니다.

2. 멤버가 주입한 값을 사용할 수 있는 경우(예를 들어 @Inject 필드가 null이 아닌 경우)를 나타냅니다.

 

Component 수명은 일반적으로 Android 클래스의 해당 인스턴스의 생성과 파괴에 의해 제한됩니다.

아래 표에는 각 구성요소에 대한 스코프 주석 및 제한 수명이 나와 있습니다.

Component Scope Created at Destroyed at
SingletonComponent @Singleton Application#onCreate() Application#onDestroy()
ActivityRetainedComponent @ActivityRetainedScoped Activity#onCreate()1 Activity#onDestroy()1
ViewModelComponent @ViewModelScoped ViewModel created ViewModel destroyed
ActivityComponent @ActivityScoped Activity#onCreate() Activity#onDestroy()
FragmentComponent @FragmentScoped Fragment#onAttach() Fragment#onDestroy()
ViewComponent @ViewScoped View#super() View destroyed
ViewWithFragmentComponent @ViewScoped View#super() View destroyed
ServiceComponent @ServiceScoped Service#onCreate() Service#onDestroy()

https://dagger.dev/hilt/components.html

 

Hilt Components

Note: The following page assumes a basic knowledge of Dagger, including components, modules, scopes, and bindings. (For a refresher, see Dagger users guide.) Component hierarchy Unlike traditional Dagger, Hilt users never define or instantiate Dagger compo

dagger.dev

 

추가 앱 구현을 하기 전에 전체 아키텍처 구성요소와 구성요소의 작동 방식을 설명하는 간단한 다이어그램을 살펴보겠습니다.

 

  • LiveData : 관찰할 수 있는 데이터 홀더 클래스, 항상 최신 버전의 데이터를 보유/캐시하고 데이터가 변경된 경우 관찰자에게 알림. LiveData는 수명 주기를 인식. UI 구성요소는 관련 데이터를 관찰하기만 하며 관찰을 중지하거나 재개하지 않음. LiveData는 관찰하는 동안 관련 수명 주기 상태의 변경을 인식하므로 이 모든 것을 자동으로 관리.
  • ViewModel : 저장소(데이터)와 UI 간의 통신 센터 역할. UI에서 더 이상 데이터의 출처에 관해 걱정하지 않아도 됨. ViewModel 인스턴스는 Activity/Fragment 재생성에도 유지됨.
  • Repository : 개발자가 만드는 클래스로, 여러 데이터 소스를 관리하는데 주로 사용
  • Entity : Room 작업 시 데이터베이스 테이블을 설명하는 주석 처리된 클래스
  • RoomDatabase : 데이터베이스 작업을 간소화하고 기본 SQLite 데이터베이스의 액세스 포인트 역할. Room 데이터베이스는 DAO를 사용하여 SQLite 데이터베이스에 쿼리를 실행
  • SQLite : 기기 내 저장소. Room 지속성 라이브러리에서 이 데이터베이스를 만들고 유지관리함.
  • DAO : 데이터 액세스 객체. SQL 쿼리를 함수에 매핑. DAO를 사용할 때 메서드를 호출하면 Room에서 나머지를 처리함.

 

아래 다이어그림은 RoomWordSample이라는 예제의 아키텍처를 보여줍니다.

앱의 모든 요소가 상호작용하는 방식을 보여주고 있습니다. 

각 그림안에 직사각형 박스(WordRoomDatabase안에 SQLite는 제외)는 개발자가 만들어야 하는 클래스들을 나타냅니다. (Observer, WordListAdapter, LiveData<List<Word>>, Word, WordDao)

 

https://developer.android.com/codelabs/android-room-with-a-view-kotlin#1

 

뷰를 사용한 Android Room - Kotlin  |  Android 개발자  |  Android Developers

이 Codelab에서는 Kotlin 코루틴과 함께 Android 아키텍처 구성요소(RoomDatabase, Entity, DAO, AndroidViewModel, LiveData)를 사용하는 Android 앱을 Kotlin으로 빌드합니다. 이 샘플 앱은 단어 목록을 Room 데이터베이스

developer.android.com

 

먼저 Entity를 만들어 보겠습니다.

 

이 앱의 데이터는 바로 Note이고 이러한 값을 보관할 간단한 테이블이 필요합니다.

 

Room을 사용하면 Enity를 통해 테이블을 만들 수 있습니다.

아래 Note는 데이터 클래스로 지난번에 Note를 저장하는데 사용하였습니다. 

이 코드를 수정해 보려고 합니다.

이 클래스는 단어의 Entity(SQLite 테이블을 나타냄)를 설명합니다.

클래스의 각 속성은 테이블의 열을 나타냅니다. Room에서는 궁극적으로 이러한 속성을 사용하여 테이블을 만들고 데이터베이스의 행에서 객체를 인스턴스화 합니다.

package com.example.jetnote.model

import java.time.LocalDateTime
import java.util.*

data class Note(
    val id: UUID = UUID.randomUUID(),
    val title: String,
    val description: String,
    val entryDate: LocalDateTime = LocalDateTime.now()
)

 

Note 클래스를 Room 데이터베이스에 의미 있게 만드려면 Kotlin 어노테이션을 사용하여 클래스와 데이터베이스 간의 연결을 만들어야 합니다.

 

우선 @Entity 어노테이션에 tableName을 설정해 줍니다.

package com.example.jetnote.model

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.time.Instant
import java.util.*

@Entity(tableName = "notes_table")
data class Note(
    @PrimaryKey
    val id: UUID = UUID.randomUUID(),
    @ColumnInfo(name = "note_title")
    val title: String,

    @ColumnInfo(name = "note_description")
    val description: String,

    @ColumnInfo(name = "note_entry_date")
    val entryDate: Date = Date.from(Instant.now())
)
  • @Entity(tableName = "notes_table") : 각 @Entity 클래스는 SQLite 테이블을 나타냅니다. 클래스 선언에 주석을 달아 Entity임을 나타냅니다. 클래스 이름과 다르게 하려면 테이블 이름을 지정하면 됩니다. 여기서는 테이블의 이름을 notes_table로 지정합니다.
  • @PrimaryKey : 모든 항목에는 기본 키가 필요합니다. 작업을 간단하게 하기 위해 각 단어는 자체 기본 키 역할을 합니다.
  • @ColumnInfo(name = "note_title") 멤버 변수 이름과 다르게 하려는 경우 테이블의 열 이름을 지정합니다. 여기서는 열 이름을 "note_title"로 지정합니다.
  • 데이터베이스에 저장된 모든 속성은 Kotlin 기본값인 public이어야 합니다.

 

이번 시간은 여기까지만 다루고 다음시간에는 DAO를 만드는 법 부터 시작해보겠습니다.

 

감사합니다.

300x250

댓글