일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- ar vr 차이
- 스택 큐 차이
- android ar 개발
- 안드로이드 레트로핏 crud
- 안드로이드 라이선스 종류
- 2022 플러터 안드로이드 스튜디오
- 객체
- 서비스 쓰레드 차이
- 안드로이드 os 구조
- 서비스 vs 쓰레드
- jvm 작동 원리
- rxjava cold observable
- 클래스
- 안드로이드 유닛테스트란
- android retrofit login
- rxjava disposable
- 안드로이드 라이선스
- ANR이란
- jvm이란
- 큐 자바 코드
- 플러터 설치 2022
- 안드로이드 유닛 테스트
- 멤버변수
- 자바 다형성
- 안드로이드 유닛 테스트 예시
- 스택 자바 코드
- rxjava hot observable
- 2022 플러터 설치
- 안드로이드 레트로핏 사용법
- Rxjava Observable
- Today
- Total
나만을 위한 블로그
[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 1 - 본문
예전에 자바로 같은 내용의 포스팅을 쓴 적이 있다.
https://onlyfor-me-blog.tistory.com/290
그러나 1년 전 글이라서 최신화가 필요하다고 생각했다. 그리고 이제 코틀린을 위주로 안드로이드 앱을 만들고 코드랩을 보던 중 Room DB와 Flow를 같이 사용하는 것을 공부해서 기록하려고 포스팅하게 됐다. 참고한 코드랩은 아래 2개다.
https://developer.android.com/codelabs/basic-android-kotlin-training-persisting-data-room?hl=ko#0
https://developer.android.com/codelabs/basic-android-kotlin-training-update-data-room?hl=ko#0
먼저 이 코드랩은 기반 코드를 깃허브에 공개해 두는데 그걸 바탕으로 직접 이것저것 추가하면서 진행하는 방식이다. 양이 많기 때문에 코드랩과 같은 순서로 포스팅을 쓰려고 한다.
- Room DB 사용 전 기반 코드 작성, 매니페스트 적용
- Room DB를 사용하기 위한 뷰모델 작성
- 아이템 추가 기능 구현 및 Database Inspector를 통한 DB 값 추가 확인
- 리사이클러뷰 + Room DB 연동
- 아이템 상세보기 기능 구현
- 아이템 수정, 삭제 기능 구현
그리고 아래 링크로 이동해서 레포를 클론한 다음 그걸 바탕으로 진행하는 게 나을 것이다. strings.xml 같은 잡다한 설정파일이나 프래그먼트 만들고 제트팩 네비게이션 처리가 귀찮은데 그 수고를 덜 수 있어서 좋다.
이제 Room DB를 사용하기 위한 data class부터 만든다. data 패키지를 만들고 그 안에 아래 클래스를 만든다.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "item")
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
아래는 어노테이션과 PK에 대한 잡담이다.
@Entity는 DB에 테이블을 만들기 위해 사용하는 어노테이션이다. tableName으로 테이블명을 정해준 다음, 생성자로 속성들을 정의하고 값을 넣으면 나중에 DB에 값을 넣을 때 여기서 정의한 속성 개수만큼 컬럼이 만들어지고 데이터들이 입력된다.
이 때 각 값들을 구분할 수 있는 기준으로 사용할 고유한 값인 기본키(Primary Key, 이하 PK)가 필요하다. 갑자기 기본키라는 게 나왔는데 이게 왜 필요한 것인가?
해외여행 가려고 호텔의 어느 마음에 드는 방을 예약했는데, 이 때 내가 받은 고객 번호가 111이라고 가정한다. 호텔에 도착해서 방 열쇠를 받으려면 고객 번호를 알려줘야 한다고 직원이 말했는데, 막상 도착하니 나와 같은 고객 번호를 받은 손님이 내가 찜했던 방을 쓰고 있다면? 또는 그 반대의 상황이 발생했다면 어떤가?
이러한 경우를 막기 위해 111이라는 숫자는 아래와 같은 특징을 가져야 한다.
- 유일해야 한다
- null이면 안 된다
- 바뀌면 안 된다
최소한 이 3가지 조건은 만족해야 위의 예시에서 든 불상사는 피할 수 있을 것이다. 그리고 저 특징들이 기본키로서 성립되기 위한 조건이다.
다시 코드로 돌아와서 어떤 값이 PK로 쓰일 수 있을까? 아이템의 이름, 가격, 재고는 위의 3가지 조건을 모두 만족하지 않는다. 만족하는 것은 id 뿐이기 때문에 Room DB가 제공하는 어노테이션 중 하나인 @PrimaryKey를 id 위에 명시한다.
그리고 테이블에 아이템이 새로 추가될 때마다 자동으로 PK를 붙여줄 수 있도록 autoGenerate를 true로 설정한다. id 외의 다른 값들은 id에 딸린 컬럼들이 될 것이기 때문에 @ColumnInfo를 붙이고, DB에 어떤 이름으로 저장될지 정한다. 여기서 정한 DB상의 이름은 나중에 Room DB 조작 함수를 다룰 때 필요하기 때문에 적절하게 붙여준다.
이제 Room DB 안의 데이터들을 조작할 함수를 만들어준다. 함수는 DAO(Data Access Object)라고 불리는 인터페이스에 만든다.
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface ItemDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
@Update
suspend fun update(item: Item)
@Delete
suspend fun delete(item: Item)
@Query("SELECT * FROM item WHERE id = :id")
fun getItem(id: Int): Flow<Item>
@Query("SELECT * FROM item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
}
아래는 DAO에 대한 잡담이다.
interface 키워드 위에 @Dao를 붙여서 이 인터페이스가 DAO라는 걸 컴파일러에게 알려주고 추상 함수를 작성할 때 @Insert, @Update, @Delete 같은 Room이 제공하는 어노테이션을 사용해서 함수를 정의한다.
직접 쿼리를 작성하고 싶다면 @Query를 쓰고 그 안에 문자열로 쿼리를 입력하면 된다. WHERE를 쓸 경우 =의 오른쪽에는 변수 앞에 콜론을 붙인다. 직접 쿼리를 입력한 getItem()의 경우 매개변수로 int값 하나를 받는데 이것을 쿼리 문자열 안에 넣으려면 변수 앞에 콜론을 붙여야 한다.
엔티티와 DAO를 만들었으니 이제 이걸 사용하는 Database 객체를 만들어야 한다. Room은 그냥 쓰는 게 아니라 Room DB 인스턴스를 싱글톤하게 만들어서 전역 Application에 등록해야만 값을 등록, 수정, 삭제하는 작업을 할 수 있게 된다.
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase: RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object {
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
fun getDatabase(context: Context): ItemRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
.fallbackToDestructiveMigration()
.build()
return instance
}
}
}
}
아래는 @Database와 소스코드에 대한 잡담이다.
@Database를 써서 해당 추상 클래스가 Database라는 걸 알려주고 소괄호를 열어서 그 안에 필요한 정보들을 넣어준다.
엔티티는 @Entity를 정의한 Item 클래스를 넣어주고, version과 exportSchema는 적절하게 넣어준다.
데이터베이스는 DAO를 알아야 하기 때문에 클래스 본문에서 ItemDao를 리턴하는 추상 함수를 만든다. 이걸 통해 액티비티, 프래그먼트에서 뷰모델을 통해 DAO 안의 함수들을 사용할 수 있게 된다.
그리고 companion object 안에 @Volatile을 명시한 변수를 만든다. 이걸 씀으로써 INSTANCE 변수는 항상 최신값을 유지할 수 있고, 값이 바뀌더라도 모든 쓰레드에 즉시 알려진다.
ItemRoomDatabase 추상 클래스를 리턴하는 getDatabse()는 INSTANCE에 값이 있다면 그걸 그대로 리턴하지만, null일 경우 synchronized() {} 안에서 초기화한 다음 리턴하게 한다. 이렇게 하면 한 번에 1개의 실행 쓰레드만 Room DB에 접근해 초기화할 수 있어 쓰레드의 경합 상태로 에러 발생이나 2개의 DB가 생성되는 걸 막을 수 있다.
이제 Application을 상속하는 클래스를 만들어 매니페스트에 등록해야 한다. Application 클래스는 앱 실행 시 가장 먼저 초기화되는 클래스기 때문에 Room DB를 여기서 초기화할 수 있다.
대신 Room DB를 사용하는 것은 몇 가지 화면을 이동해서인 경우가 있을 수 있기 때문에 lazy 위임을 사용해서 참조가 처음 필요할 때(처음 참조에 접근할 때) 초기화해서 Room DB를 사용할 때 DB가 초기화되도록 한다.
import android.app.Application
import com.example.kotlinprac.room.data.ItemRoomDatabase
class InventoryApplication: Application() {
val database: ItemRoomDatabase by lazy {
ItemRoomDatabase.getDatabase(this)
}
}
이렇게 만든 Application은 매니페스트에 아래와 같이 등록한다.
<application
android:name=".room.InventoryApplication"
이제 나중에 액티비티, 프래그먼트에서 Room DB 조작 함수를 사용할 때 뷰모델을 통해 사용할 건데, 그 때 DAO 인터페이스의 함수를 사용할 수 있게 된다.
'Android' 카테고리의 다른 글
[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 2 - (0) | 2022.12.13 |
---|---|
[Android] Flow vs LiveData (0) | 2022.12.12 |
[Android] Navigation 사용 시 FragmentDirections가 자동 생성되지 않을 때 (0) | 2022.12.09 |
[Android] MVP vs MVVM (0) | 2022.12.05 |
[Android] EventBus란? EventBus 사용법 (0) | 2022.11.21 |