본문 바로가기

Dailycoding/Android

wifi 없을 때 Room 사용해서 화면에 표현

결과 화면

 

 

파일 구조
Room 데이터

 


기본 구성요소

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