@Entity
data class Schedule(
@PrimaryKey val id: Int,
@NonNull @ColumnInfo(name = "stop_name") val stopName: String,
@NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int
)
위 데이터 클래스에서 컬럼이 스네이크 케이스로 명명되어있지만, 코틀린에서는 카멜 케이스로 명명하기 때문에 @ColumnInfo
주석을 활용해 이름을 지정했다.
@Dao
interface ScheduleDao {
@Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC")
fun getByStopName(stopName: String): List<Schedule>
}
그리고 Dao 쿼리에서 값을 참조하기 위해 :
를 사용한다.
ViewModelFactory
는 ViewModel
인스턴스를 생성하는 데 사용되는 클래스다. ViewModelProvider
를 사용하여 ViewModel
인스턴스를 만들고 관리한다.
ViewModelFactory
는 ViewModel
의 인스턴스화를 위해 필요한 데이터 또는 객체를 전달할 수 있도록 도와준다. 그래서 ViewModel
이 생성될 때마다 인스턴스화해야 하는 클래스 또는 데이터가 있을 때 유용하다. 예를 들어 ViewModelFactory
를 사용하여 데이터베이스 인스턴스를 ViewModel에 전달하면 ViewModel은 데이터베이스를 사용하여 데이터를 검색하거나 업데이트 할 수 있다.
일반적으로 앱의 Application 클래스에서 인스턴스화되며, 필요한 경우에는 액티비티나 프래그먼트에서 사용되기 때문에 이를 통해 ViewModel의 생명주기와 관련된 문제를 해결할 수 있다.
ViewModelFactory
의 장점은 다음과 같이 정리할 수 있다.ViewModelFactory
를 사용하지 않을 때는 다음과 같은 문제점들이 생긴다.ViewModelFactory
를 사용하는 법을 코드로 알아보자.
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")
}
}
이 코드는 BusScheduleViewModelFactory 클래스에 전달된 클래스가 BusScheduleViewModel 클래스의 하위 클래스인 경우, ScheduleDao 인스턴스를 사용하여 BusScheduleViewModel 인스턴스를 생성한다. 그렇지 않은 경우 IllegalArgumentException을 발생시킨다.
이를 통해 BusScheduleViewModelFactory 클래스가 BusScheduleViewModel 인스턴스 생성을 담당하고, 다른 ViewModel 클래스에 대해서는 예외를 발생시키는 역할을 하게 된다.
즉, ViewModel 인스턴스 생성에 대한 책임을 ViewModelProvider.Factory에서 BusScheduleViewModelFactory로 이전하여, ViewModel 인스턴스 생성과 의존성 주입을 효율적으로 처리할 수 있게 됐다.
create()
ViewModelProvider가 ViewModel을 요청할 때 호출. 요청된 ViewModel 클래스와 ScheduleDao 인스턴스를 사용하여 ViewModel 인스턴스를 생성하고 반환함.isAssignableFrom()
인자로 전달된 클래스가 다른 클래스의 하위 클래스인지 여부를 반환함. 이 코드에서는 전달된 클래스가 BusScheduleViewModel 클래스의 하위 클래스인지 검사.그렇다면 항상 ViewModelFactory를 쓰는 게 좋을까? 이 질문의 답은 No다.
ViewModelFactory를 사용하면 성능에 영향을 줄 수도 있다. ViewModelFactory를 사용하면 ViewModel 인스턴스를 생성할 때 추가 오버헤드가 발생하기 때문이다. ViewModel 인스턴스를 자주 생성하거나 생성하는 데 많은 시간이 걸리는 경우에는 성능 저하가 발생할 수 있다는 말이다.
그러니까 정리하면 ViewModel Factory를 사용하는 것이 항상 좋은 것은 아니다. ViewModel 인스턴스를 자주 생성하거나 생성하는 데 많은 시간이 걸리는 경우에는 성능을 저하시킬 수 있기 때문에, 앱의 요구사항을 고려하여 ViewModel Factory를 사용할지 여부를 결정하자.
kotlin_app_programming_tutorial/UserDatabase.kt at main · HS0204/kotlin_app_programming_tutorial
위 포스트와 코드에서도 공부를 했지만 하나하나씩 뜯어보며 다시 살펴보자. 우선 전체적인 코드다.
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()
INSTANCE = instance
return instance
}
}
}
}
abstract class AppDatabase: RoomDatabase()
room 라이브러리가 따로 구현하기 때문에 추상 클래스로 만든다.abstract fun scheduleDao(): ScheduleDao
companion object
경합 상태나 기타 잠재적 문제를 방지하려고 데이터베이스 인스턴스가 하나만 있는지 확인해야 한다. 이를 위해 인스턴스를 companion object에 저장하고, 기존 인스턴스를 반환하거나 새 인스턴스를 만드는 메서드가 필요하다.우선 인스턴스를 만든다.
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
@Volatile
주석을 사용하면 해당 변수의 모든 읽기와 쓰기는 메인 메모리에서 실행하게 된다. 그래서 변수의 값이 최신으로 유지될 수 있고, 다른 스레드에서 활용되고 있더라도 이 변수를 즉각적으로 표시하기 때문에 선언한다.
인스턴스를 반환하는 메서드를 만든다. 하나만 있으면 되니까 싱글톤이어야 한다.
fun getDatabase(context: Context): ItemRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
).fallbackToDestructiveMigration().build()
INSTANCE = instance
return instance
}
}
엘비스 연산자 ?:
를 사용해 인스턴스가 원래 기존에 있는지 아닌지 확인한다. 이미 있다면 기존 인스턴스를 반환하고, 아니라면 데이터베이스 인스턴스를 만들게 된다. 중복 생성을 방지하기 위해 여러 스레드에서 요청되더라도 한 번에 하나만 안전하게 액세스할 수 있는 synchronized()
를 사용했고, createFromAsset()
를 호출해 기존 데이터를 로드한다.
fallbackToDestructiveMigration()
는 데이터가 손실되지 않도록 이전 스키마의 모든 행을 가져와 새 스키마의 행으로 변환하는 방법을 정의한다.
@Datebase
데이터베이스임을 알리는 주석. 엔티티는 여러 개일 수 있고, 배열 형태로 나열된다. 버전은 스키마가 변경될 때마다 증가하도록 하자.RecyclerView를 만들 때 기존에는 RecyclerView.Adapter
를 사용했지만, item 하나에 변경사항이 일어나면 실시간으로 처리할 때 전체 recyclerView를 새로고침하기 때문에 좋지 않다. 동적으로 list가 변경될 때 성능이 좋지 않다는 말이다.
이런 단점의 대안이 **ListAdapter**
다. AsyncListDiffer
를 활용해서 이전 데이터 item과 바뀐 데이터 item을 확인해 차이를 알고, 이 차이를 통해 바뀐 item만 업데이트를 하기에 효율적이다. 그러니까 자주 업데이트 되는 데이터를 처리하기 매우 좋다.
kotlin_app_programming_tutorial/PhotoGridAdapter.kt at main · HS0204/kotlin_app_programming_tutorial
위 코드에서 이미 적용을 했었지만 조금 다른 코드를 예시로 다시 보자.
class BusStopAdapter(private val onItemClicked: (Schedule) -> Unit) : ListAdapter<Schedule, BusStopAdapter.BusStopViewHolder>(DiffCallback) {
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))
}
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)
)
}
}
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
}
}
}
}
onItemClicked()
: setOnClickListener()
와 함께 아이템을 클릭했을 때 그 아이템의 위치를 알려준다.onBindViewHolder()
: 지정 위치에 뷰를 바인딩한다.ViewHolder()
: 뷰에 액세스한다.DiffCallback
: ListAdapter
가 list를 업데이트할 때 어떤 item이 달라졌는지 확인한다. 이 코드에는 id만 확인하는 메서드(areItemsTheSame), item 자체를 확인하는 메서드(areContetnsTheSame) 두 가지가 있다.Flow는 데이터 스트림을 반환하는 기능으로, 이를 이용해서 데이터베이스 INSERT와 같은 실시간 업데이트에 대응해보자.
@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>
}
이 Dao를 사용하면 데이터가 변경되어도 동적 업데이트를 할 수 없다. 왜냐하면 반환값이 List로, Dao에서 한 번만 반환이 되기 때문이다. 따라서 다음과 같이 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>>
}
반환 유형이 flow기 때문에 따로 suspend를 명시할 필요는 없다./*
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
fun fullSchedule(): List<Schedule> = scheduleDao.getAll()
fun scheduleForStopName(name: String): List<Schedule> = scheduleDao.getByStopName(name)
}
*/
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
fun fullSchedule(): Flow<List<Schedule>> = scheduleDao.getAll()
fun scheduleForStopName(name: String): Flow<List<Schedule>> = scheduleDao.getByStopName(name)
}
GlobalScope.launch(Dispatchers.IO) {
// submitList() is a call that accesses the database. To prevent the
// call from potentially locking the UI, you should use a
// coroutine scope to launch the function. Using GlobalScope is not
// best practice, and in the next step we'll see how to improve this.
busStopAdapter.submitList(viewModel.fullSchedule())
}
주석에서와 같은 문제가 있기 때문에 다음과 같이 바꿔준다.lifecycle.coroutineScope.launch {
viewModel.fullSchedule().collect() {
busStopAdapter.submitList(it)
}
}