기본 구성요소
Room에는 다음 3가지 주요 구성요소가 있습니다.
- 데이터베이스 클래스: 데이터베이스를 보유하고 앱의 영구 데이터와의 기본 연결을 위한 기본 액세스 포인트 역할을 합니다.
- 데이터 항목: 앱 데이터베이스의 테이블을 나타냅니다.
- 데이터 액세스 객체(DAO): 앱이 데이터베이스의 데이터를 쿼리, 업데이트, 삽입, 삭제하는 데 사용할 수 있는 메서드를 제공합니다.
1️⃣ 데이터 항목 (Entity)
- 항상@Entity 주석을 달아야 합니다
- data class로 선언
- 기본적으로 Room은 클래스 이름을 데이터베이스 테이블 이름으로 사용합니다. 테이블 이름을 다르게 하려면 @Entity 주석의 tableName 속성을 설정하세요. 마찬가지로 Room은 기본적으로 필드 이름을 데이터베이스의 열 이름으로 사용합니다. 열 이름을 다르게 하려면 @ColumnInfo 주석을 필드에 추가하고 name 속성을 설정하세요.
- 기본키 설정 : 단일 열에 @PrimaryKey로 주석을 다는 것
참고: Room에서 항목 인스턴스에 자동 ID를 할당하게 하려면 @PrimaryKey의 autoGenerate 속성을 true로 설정하세요.
@Entity(tableName = "plants")
data class Plant(
@PrimaryKey
val plantId : String,
val name : String,
val description : String,
val growZoneNumber : Int,
val wateringInterval : Int,
val imageUrl : String
)
2️⃣ 데이터 액세스 객체 (Dao)
- 항상 @Dao 주석을 달아야 합니다
- 인터페이스나 추상 클래스로 정의할 수 있습니다.
- DAO 메서드에는 두 가지 유형이 있습니다.
- 편의 메서드: SQL 코드 작성 없이 데이터베이스에서 행을 삽입하고 업데이트하고 삭제할 수 있습니다.
- ex) @Insert, @Delete, @Update
- 쿼리 메서드: 자체 SQL 쿼리를 작성하여 데이터베이스와 상호작용할 수 있습니다.
- OnConflict 인수는 충돌이 발생하는 경우 Room에 실행할 작업을 알려줍니다. OnConflictStrategy.IGNORE 전략은 기본 키가 이미 데이터베이스에 있으면 새 항목을 무시합니다.
@Dao
interface PlantDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPlants(plants: List<Plant>)
@Query("SELECT * FROM plants")
suspend fun getAllPlants(): List<Plant>
}
3️⃣ 데이터베이스(Database)
설명: Database 클래스는 개발자가 정의한 DAO의 인스턴스를 앱에 제공합니다. 결과적으로 앱은 DAO를 사용하여 데이터베이스의 데이터를 연결된 데이터 항목 객체의 인스턴스로 검색할 수 있습니다. 앱은 정의된 데이터 항목을 사용하여 상응하는 테이블의 행을 업데이트하거나 삽입할 새 행을 만들 수도 있습니다.
- @Database 주석이 달린 추상 RoomDatabase 클래스를 만들어야 합니다. 이 클래스에는 RoomDatabase의 인스턴스를 만들거나(없는 경우) RoomDatabase의 기존 인스턴스를 반환하는 메서드가 하나 있습니다.
- entities , version 설정해 주기
- 데이터베이스는 DAO를 알아야 합니다. DAO를 반환하는 추상 함수를 선언합니다. DAO는 여러 개가 있을 수 있습니다.
- 싱글톤 디자인 패턴을 따라야 합니다.
@Database(entities = [Plant::class], version = 1)
abstract class PlantRoomDatabase : RoomDatabase() {
abstract fun plantDao(): PlantDao
companion object {
@Volatile
private var INSTANCE: PlantRoomDatabase? = null // 싱글톤 인스턴스
fun getDatabase(context: Context): PlantRoomDatabase {
return INSTANCE ?: synchronized(this) {
// INSTANCE가 null일 경우에만 데이터베이스를 생성
val instance = Room.databaseBuilder(
context.applicationContext,
PlantRoomDatabase::class.java,
"plant_database" // 데이터베이스 이름
)
.fallbackToDestructiveMigration() // 마이그레이션이 없을 경우 기존 데이터 삭제
.build()
INSTANCE = instance
instance
}
}
}
}
Application 클래스 구현
- MyApplication.kt를 열고 PlantRoomDatabase 유형의 database라는 val을 만듭니다. 콘텍스트를 전달하는 PlantRoomDatabase에서 getDatabase()를 호출하여 database 인스턴스를 인스턴스화합니다. lazy 위임을 사용하므로 참조가 처음 필요하거나 처음 참조에 액세스 할 때(앱이 시작될 때가 아니라) database 인스턴스가 느리게 만들어집니다. 이렇게 하면 처음 액세스할 때 데이터베이스(디스크의 물리적 데이터베이스)가 만들어집니다.
class MyApplication : Application() {
// Room 데이터베이스 초기화
val database: PlantRoomDatabase by lazy { PlantRoomDatabase.getDatabase(this) }
}
- 이렇게 1 ~ 3번까지 하면 room에 대한 설정을 마무리할 수 있다.
4️⃣ Repository
class MainRepository(private val plantDao: PlantDao) {
private val client = RetrofitInstance.getInstance().create(GitApi::class.java)
suspend fun getAllData() = client.getAllPlants()
// Room에 데이터 삽입
suspend fun insertPlants(plants: List<Plant>) {
plantDao.insertPlants(plants)
}
// Room에서 데이터 가져오기
suspend fun getPlantsFromDatabase(): List<Plant> {
return plantDao.getAllPlants()
}
}
5️⃣ ViewModel
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {
private val _allPlants = MutableLiveData<Response<List<Plant>>>()
val allPlants: LiveData<Response<List<Plant>>> get() = _allPlants
fun getAllData() {
viewModelScope.launch {
try {
val response = mainRepository.getAllData()
Log.d("MainViewModel", "getAllData response: $response")
Log.d("MainViewModel", "getAllData response.body(): ${response.body()}")
if (response.isSuccessful) {
response.body()?.let { plants ->
// Room에 데이터 삽입
mainRepository.insertPlants(plants)
}
_allPlants.postValue(response)
} else {
Log.e("MainViewModel", "Error: ${response.message()}")
val plantsFromDb = mainRepository.getPlantsFromDatabase()
_allPlants.postValue(Response.success(plantsFromDb))
}
} catch (e: Exception) {
Log.e("MainViewModel", "Error fetching data: ${e.message}", e)
val plantsFromDb = mainRepository.getPlantsFromDatabase()
_allPlants.postValue(Response.success(plantsFromDb))
}
}
}
}
- if 일 때, 서버로부터 데이터를 정상적으로 받아와 지면 Room에 데이터를 할당해 주기
- else, catch 일 때, Room에 할당된 데이터가 있다면, _allPlants에 할당 이렇게 하면 wifi 나 서버로부터 데이터를 받아오기를 실패했을 경우에도 Room에 저장된 데이터를 불러와서 화면에 표시가 가능해진다.
6️⃣ MainViewModelFactory
class MainViewModelFactory(private val repository: MainRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return MainViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
- ViewModelFactory는 ViewModel에 매개변수가 있을 때 사용하는 패턴입니다. 기본적으로 ViewModel은 매개변수를 받지 않는 기본 생성자를 사용하여 생성되지만, 비즈니스 로직이나 데이터 소스와 같은 의존성을 주입할 필요가 있을 때는 매개변수를 받을 수 있습니다.
7️⃣ MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var adapter: MainAdapter
private val viewModel: MainViewModel by viewModels {
MainViewModelFactory(MainRepository((application as MyApplication).database.plantDao()))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
initUi()
observeData()
}
private fun initUi() {
binding.run {
adapter = MainAdapter(emptyList())
recyclerViewMain.adapter = adapter
recyclerViewMain.layoutManager = LinearLayoutManager(this@MainActivity)
viewModel.getAllData()
}
}
private fun observeData() {
viewModel.allPlants.observe(this) { result ->
if (result.isSuccessful) {
result.body()?.let { plants ->
adapter.updateData(plants)
Log.d("MainActivity1", "Plants fetched: $plants")
}
} else {
Log.d("MainActivity1", "Error fetching data: ${result.message()}")
}
}
}
}
8️⃣ MainAdapter
class MainAdapter(private var dataList: List<Plant>) : RecyclerView.Adapter<MainAdapter.MainViewHolder>() {
inner class MainViewHolder(private val binding: ItemMainBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Plant) {
binding.apply {
textViewItemName.text = item.name
textViewItemDescription.text = item.description
Glide.with(itemView.context)
.load(item.imageUrl)
.into(imageViewItemMain)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
val binding = ItemMainBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MainViewHolder(binding)
}
override fun getItemCount(): Int {
return dataList.size
}
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
holder.bind(dataList[position])
}
fun updateData(newDataList: List<Plant>) {
this.dataList = newDataList
notifyDataSetChanged()
}
}
전체 코드
Blog/Project/Room at develop · wjdwntjd55/Blog
Contribute to wjdwntjd55/Blog development by creating an account on GitHub.
github.com
참고한 자료
Room을 사용하여 로컬 데이터베이스에 데이터 저장 | Android Developers
Room 라이브러리를 사용하여 더 쉽게 데이터를 유지하는 방법 알아보기
developer.android.com
Room을 사용하여 데이터 유지 | Android Developers
Android Kotlin 앱에서 Room을 사용하는 방법을 알아보세요. Room은 Android Jetpack의 일부인 지속성 데이터베이스 라이브러리로, SQLite 위에 있는 추상화 레이어입니다. Room은 데이터베이스를 설정하고 구
developer.android.com
Room을 사용하여 데이터 읽기 및 업데이트 | Android Developers
Room을 사용하여 Android Kotlin 앱에서 데이터를 읽고 업데이트하는 방법을 알아보세요. Room은 Android Jetpack의 일부인 데이터베이스 라이브러리로, 데이터베이스 설정 및 구성과 같은 여러 작업을 처
developer.android.com
'Dailycoding > Android' 카테고리의 다른 글
Data Binding (0) | 2024.08.27 |
---|---|
CoordinatorLayout (0) | 2024.08.21 |
shimmer library를 사용해서 스켈레톤 로딩 화면 (0) | 2024.06.11 |
Viewpager2 활용해서 banner 구현 (0) | 2024.06.01 |
Fragment 화면 전환 시 상태 유지하기 (0) | 2024.05.29 |