일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Dialog
- CustomScrollView
- 안드로이드
- intent
- tabbar
- LifeCycle
- DART
- appbar
- scroll
- android
- 앱
- TEST
- 계측
- viewmodel
- data
- binding
- Navigation
- Flutter
- Coroutines
- livedata
- activity
- Button
- ScrollView
- 테스트
- drift
- 앱바
- Compose
- textfield
- Kotlin
- textview
- Today
- Total
Study Record
[안드로이드] Room (database) 살펴보기 (ViewModel, Flow, ListAdapter, LiveData, Coroutines) 본문
[안드로이드] Room (database) 살펴보기 (ViewModel, Flow, ListAdapter, LiveData, Coroutines)
초코초코초코 2023. 8. 19. 00:51😶 Room 개요
데이터베이스를 안드로이드에서 쉽게 사용할 수 있는 방법은 Room 라이브러리를 사용하는 것이다. Room 은 Android Jetpack 의 일부인 지속성 라이브러리로 SQLite 데이터베이스를 관리하는 추상화 계층으로 작업 수행을 위한 특수 언어인 SQL 을 사용한다.
Room 은 ORM(Oject Relational Mapping) 으로, 객체형 데이터베이스의 테이블을 Kotlin 에서 사용할 수 있는 객체와 매핑할 수 있다. 즉, 각 테이블은 클래스로 표시하고 이러한 테이블을 모델 클래스 혹은 엔티티(entity)라고 부른다.
데이터베이스에 사용되는 SQL 은 실제로 INTEGER, TEXT 로 표시된다. 하지만 Kotlin 의 데이터 유형은 Int, String 이다. 이러한 데이터 유형을 매핑하는 작업은 자동으로 Room 이 처리해 준다.
Room 의 주요 컴포넌트에는 Data Entity, DAO(Data Access Objects), Database Class 가 있다.
Data Entity 는 위에서 설명한 것과 같이 테이블을 정의한다.
DAO(Data Access Objects) 는 데이터베이스의 데이터 삽입, 삭제, 업데이트 등과 관련된 메서드를 제공한다.
Database Class 는 앱의 데이터베이스에 대한 기본 접근을 위한 클래스로 데이터베이스와 관련된 DAO 의 인스턴스도 같이 제공한다.
😶 Room 사용하기
dependency 추가하기
dependencies {
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
}
table 선언하기
Room 에서 테이블을 모델 클래스 혹은 엔티티(entity)라고 불리며 클래스로 표시할 수 있다.
import androidx.annotation.NonNull
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName="mySchedule")
data class Schedule(
@PrimaryKey(autoGenerate = true)
val id: Int,
@NonNull @ColumnInfo(name = "stop_name") val stopName: String,
@NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int
)
@Entity 어노테이션을 붙이면 테이블로 정의할 수 있다. Room 은 대소문자를 구분하지 않는다.
DAO(Data Access Object) 클래스 선언하기
DAO 는 데이터에 대한 엑세스를 제공하는 Kotlin 클래스로 추상적인 인터페이스를 제공한다. 데이터베이스에서 데이터를 읽고 수정하는 등의 기능이 포함되며 데이터베이스를 다루는 방법은 SQL 문법을 통해 가능하다. DAO 는 데이터베이스 작업을 수행하는 모든 복잡한 작업을 숨겨 실제 데이터 접근 계층이 DAO 에 정의된 코드와 서로 독립적으로 변경된다. 따라서, DAO 를 작성하는 것으로 실제 데이터베이스의 내용을 변경할 수 있다.
@Dao
interface ScheduleDao {
@Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
fun getAll(): List<Schedule>
@Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC")
fun getByStopName(stopName: String): List<Schedule>
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(schedule: Schedule)
@Delete
fun delete(schedule: Schedule)
@Update
fun update(schedule: Schedule)
}
@Query 어노테이션으로 SQL 구문을 작성할 수 있다. SQL 구문에 ":" 기호를 사용하여 변수를 넣을 수 있다.(ex. :stop_name)
데이터베이스 선언하기
데이터베이스 클래스는 엔티티 및 데이터 액세스 개체의 목록을 정의한다. 정의한 DAO 의 인스턴스를 앱에 제공해 이 데이터베이스 클래스의 DAO 를 사용하여 테이블 행을 업데이트하거나 삽입할 수 있다.
추상 클래스로 RoomDatabase 를 상속받고 @Database 어노테이션을 선언한다. 엔티티(entities)정보에 데이터베이스에 포함될 테이블을 정의해주고, version 으로 현재 데이터베이스 버전을 적어준다. Room 라이브러리가 개발자가 정의한 RoomDatabase 를 상속받은 추상 클래스를 참고로 따로 구현할 것이기 때문에 추상 클래스로 작성한다.
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.busschedule.database.schedule.Schedule
import com.example.busschedule.database.schedule.ScheduleDao
@Database(entities = arrayOf(Schedule::class), version = 1)
abstract class AppDatabase: RoomDatabase() {
abstract fun scheduleDao(): ScheduleDao
}
DAO 를 클래스 내 추상 클래스로 선언해두면 나중에 이 메서드를 통해 DAO 를 참조할 수 있다.
데이터베이스 클래스에는 DAO 뿐만 아니라 데이터베이스 인스턴스를 제공한다. 전체 앱에서 데이터베이스 인스턴스는 하나만 필요하므로 companion object 를 사용한다. INSTANCE 변수는 데이터베이스가 생성됐을 때 그에 대한 참조를 유지한다. @Volatile 어노테이션은 휘발성 변수의 값은 캐시되지 않으며 주 메모리에서 모든 읽기와 쓰기가 수행된다. 즉, INSTANCE 변수의 값에 대한 변경 사항이 다른 모든 스레드에 즉시 표시된다는 것을 의미한다.
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.busschedule.database.schedule.Schedule
import com.example.busschedule.database.schedule.ScheduleDao
@Database(entities = arrayOf(Schedule::class), version = 1)
abstract class AppDatabase: RoomDatabase() {
abstract fun scheduleDao(): ScheduleDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"schedule_database")
//.createFromAsset("database/bus_schedule.db")
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
여러 개의 스레드가 동시에 실행되어 두 개의 데이터베이스가 생성될 가능성이 있다. 따라서, synchronized{} 블록을 사용하면 하나의 스레드만 블록을 실행할 수 있어 데이터베이스가 한 번만 초기화되도록 한다. createFormatAsset() 메서드로 asset 디렉터리에 있는 파일로 데이터베이스를 초기화할 수 있다.
Application 는 앱 전역의 상태를 포함하는 기본 클래스로 앱 프로젝트에서 전역 변수로 사용할 수 있다. 이 클래스에 database 변수를 선언한다.
class BusScheduleApplication: Application() {
val database: AppDatabase by lazy {
AppDatabase.getDatabase(this)
}
}
Application 클래스가 AnroidManifest.xml 파일에 선언되어있지 않다면 추가해 준다.
<application
android:name=".BusScheduleApplication"
...
ViewModel 에서 DAO 접근하기
ViewModel 클래스가 라이프사이클을 인식하도록 되어있으므로 라이프사이클 이벤트에 응답할 수 있는 객체에 의해 인스턴스화되어야 한다. 이를 직접 인스턴스화한다면 메모리 관리를 포함하여 신경 써야 할 점이 많으니 ViewModel 객체를 인스턴스화하는 팩토리 클래스를 이용할 수 있다.
class BusScheduleViewModel(private val scheduleDao: ScheduleDao) : ViewModel() {
fun fullScheduleNotFlow(): List<Schedule> = scheduleDao.getAll()
fun scheduleForStopNameNotFlow(name: String): List<Schedule> = scheduleDao.getByStopName(name)
fun updateItem(item: Schedule) {
viewModelScope.launch(Dispatchers.IO) { itemDao.update(item) }
}
fun deleteItem(item: Item) {
viewModelScope.launch(Dispatchers.IO) { itemDao.delete(item) }
}
fun insertItem(item: Item) {
viewModelScope.launch(Dispatchers.IO) { itemDao.insert(item) }
}
}
class BusScheduleViewModelFactory(
private val scheduleDao: ScheduleDao
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(BusScheduleViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return BusScheduleViewModel(scheduleDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Fragment 에서 ViewModel 선언 및 사용하기
정의한 ViewModelFactory 를 사용하여 viewModel 을 선언할 수 있다. 데이터베이스에서 데이터를 가져오는 작업은 모두 시간이 걸리는 작업일 수 있기 때문에 메인 스레드에서 실행하는 것은 좋지 않다. 따라서 코루틴(스레드)를 사용한다.
class StopScheduleFragment: Fragment() {
private val viewModel: BusScheduleViewModel by activityViewModels {
BusScheduleViewModelFactory(
(activity?.application as BusScheduleApplication).database.scheduleDao()
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycle.coroutineScope.launch(Dispatchers.IO) {
viewModel.scheduleForStopNameNotFlow(stopName)
}
}
}
😶 Room + ViewModel + ListAdapter + Flow + LiveData
데이터베이스를 사용한 프로그램은 데이터가 실시간으로 변경될 가능성이 크다.
ListAdapter
데이터 목록을 보여주는 RecyclerView 에서 사용하는 어댑터는 RecyclerView.Adapter 이다. 이 어댑터는 데이터가 변경될 때 데이터 리스트를 다시 명시하면 데이터 목록 전체를 다시 새로 고침 한다. 이 과정은 불필요할 수 있다.
RecyclerView.Adapter 를 상속받는 어댑터 중 하나인 ListAdapter 를 사용하면 데이터가 변경될 때 이전 데이터 목록과 비교하여 차이가 있는 항목만 업데이트되므로 데이터가 실시간으로 변경되는 프로그램에서는 ListAdapter 가 적합하다.
예시 코드)
class BusStopAdapter(
private val onItemClicked: (Schedule) -> Unit
) : ListAdapter<Schedule, BusStopAdapter.BusStopViewHolder>(DiffCallback){
companion object {
private val DiffCallback = object : DiffUtil.ItemCallback<Schedule>() {
override fun areItemsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
return oldItem == newItem
}
}
}
class BusStopViewHolder(
private var binding: BusStopItemBinding
) : RecyclerView.ViewHolder(binding.root){
@SuppressLint("SimpleDateFormat")
fun bind(schedule: Schedule) {
binding.stopNameTextView.text = schedule.stopName
binding.arrivalTimeTextView.text = SimpleDateFormat(
"h:mm a").format(Date(schedule.arrivalTime.toLong() * 1000))
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BusStopViewHolder {
val viewHolder = BusStopViewHolder(
BusStopItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
viewHolder.itemView.setOnClickListener {
val position = viewHolder.adapterPosition
onItemClicked(getItem(position))
}
return viewHolder
}
override fun onBindViewHolder(holder: BusStopViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
/* 데이터 업데이트
val adapter = BusStopAdapter() {}
adapter.submitList(listOf())
*/
Flow
데이터베이스의 데이터가 변경되면(업데이트, 삭제, 삽입) 당연히 데이터를 참조한 부분도 전부 변경해야 한다. 데이터가 변경되고 다시 데이터를 불러오는 등 후속 작업을 전부 개발자가 감당하는 것은 오류가 날 확률도 높아지고 복잡한 과정이 될 것이다.
Flow 를 사용하면 데이터베이스의 데이터가 변경되면 관련 데이터가 자동으로 다시 업데이트되도록 도와준다.
사용 방식은 간단하게, DAO 의 리턴값을 Flow 타입으로 감싼다.
import androidx.room.Dao
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface ScheduleDao {
@Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
fun getAll(): Flow<List<Schedule>>
@Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC")
fun getByStopName(stopName: String): Flow<List<Schedule>>
}
또한, ViewModel 의 데이터를 가져오는 함수의 리턴값도 마찬가지로 Flow 타입으로 감싸준다.
import kotlinx.coroutines.flow.Flow
class BusScheduleViewModel(private val scheduleDao: ScheduleDao) : ViewModel() {
fun fullSchedule(): Flow<List<Schedule>> = scheduleDao.getAll()
fun scheduleForStopName(name: String): Flow<List<Schedule>> = scheduleDao.getByStopName(name)
}
Fragment 에서 데이터를 가져와 View 를 수정하는 부분을 다음과 같이 사용할 수 있다. 프래그먼트에서 코루틴을 사용하여 ViewModel 을 통해 데이터를 가져오는데 collect() 메서드를 통해 데이터가 변경사항이 있어 변경된다면 자동으로 콜백 해준다. 콜백 내용에 데이터와 관련된 View 의 수정코드를 적는다.
class StopScheduleFragment: Fragment() {
private val viewModel: BusScheduleViewModel by activityViewModels {
BusScheduleViewModelFactory(
(activity?.application as BusScheduleApplication).database.scheduleDao()
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycle.coroutineScope.launch(Dispatchers.IO) {
viewModel.scheduleForStopName(stopName).collect() { it ->
// 데이터가 변경될 때마다 실행된다.
busStopAdapter.submitList(it)
}
}
}
}
LiveData + Flow
LiveData 는 데이터의 변경을 감지하고 콜백 해주는 옵져버를 사용할 수 있다. 위에서 소개한 Flow 는 관련된 데이터베이스의 데이터가 변경될 때마다 콜백 해준다. Flow 를 LiveData 로 변경하는 걸 지원하기 때문에 좀 더 간단하게 코드를 정리할 수 있다.
class BusScheduleViewModel(private val scheduleDao: ScheduleDao) : ViewModel() {
val scheduleItems: LiveData<List<Schedule>> = scheduleDao.getAll().asLiveData()
}
Fragment 애서 사용할 때, 다음과 같다. LiveData 의 observer 콜백을 정의하는 것으로 대체한다.
class StopScheduleFragment: Fragment() {
private val viewModel: BusScheduleViewModel by activityViewModels {
BusScheduleViewModelFactory(
(activity?.application as BusScheduleApplication).database.scheduleDao()
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/* Flow 만 사용할 경우
lifecycle.coroutineScope.launch(Dispatchers.IO) {
viewModel.scheduleForStopName(stopName).collect() { it ->
// 데이터가 변경될 때마다 실행된다.
busStopAdapter.submitList(it)
}
}*/
// LiveData Observer Setting
viewModel.scheduleItems.observe(this.viewLifecycleOwner) {
busStopAdapter.submitList(it)
}
}
}
'안드로이드' 카테고리의 다른 글
[안드로이드] DataSotre (SharedPreferences 대체) (0) | 2023.08.21 |
---|---|
[안드로이드] Repository pattern 과 Caching (0) | 2023.08.21 |
[안드로이드] RecyclerView ListAdapter 살펴보기 (0) | 2023.08.10 |
[안드로이드] binding Adapter (결합 어댑터) (0) | 2023.08.10 |
[안드로이드] Retrofit 살펴보기 (ViewModel, Moshi, coroutines) (0) | 2023.08.09 |