Unit 5: Data persistence (1)

quokka·2021년 11월 27일
0

Android Basics in Kotlin

목록 보기
21/25
post-thumbnail

관계형 데이터베이스

관계형 데이터베이스는 데이터를 테이블과 열, 행으로 구성하는 일반적인 데이터베이스 유형이다. 관계형 데이터베이스에서 테이블은 데이터를 나타내는 것 외에도 다른 테이블을 참조하거나 테이블 간 관계를 확인 할 수 있다.

열, 행

은 사물의 특정 속성
은 테이블에 저장된 실제 데이터

기본 키

테이블 행의 고유 식별자 역할을 하는 것을 키본 키라고 한다. 기본 키는 다른 테이블에서 참조할 때 유용하다.

SQL

SQL(sequel)은 구조화된 쿼리 언어를 의미하며 이를 통해 관계형 데이터베이스에서 데이터를 읽고 조작할 수 있다.
가장 일반적으로 사용하는 SQL문에는 SELECT INSERT UPDATE DELETE가 있다.

Database Inspector

안드로이드의 database inspector를 사용해 쿼리를 실행할 수 있다.

Android 스튜디오 2020.3.1 Arctic Fox를 사용하고 있다면 View > Tool > App Inspection을 통해 Database Inspector에 액세스할 수 있습니다.

SQL 기본 익히기

// 처음 5개만 반환하도록 limit
SELECT name FROM park
LIMIT 5

// != 연산자 사용
SELECT name FROM park
WHERE type != "recreation_area"
AND area_acres > 100000

// COUNT() SUM() MAX() 등 여러 함수
SELECT MAX(area_acres) FROM park
WHERE type = 'national_park'

// GROUP BY, ORDER BY
SELECT type, name FROM park
GROUP BY type
ORDER BY name

// INSERT, UPDATE, DELETE

Room

앱에서 Room을 사용하도록 데이터베이스 클래스를 정의하고, 데이터베이스를 채운다.

1. build.gradle

Room을 사용하려면 gradle에 다음 항목을 추가해야 한다.
build.gradle (project)

ext{
	room_version = '2.3.0'
}

build.gradle (module)

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"

2. Schedule 테이블

Room에서 각 테이블은 클래스로 표시된다. Bus Schedule 앱은 Schedule이라는 단일 테이블로 구성된다.
database.schedule 패키지 아래 Schedule.kt 파일을 생성하고 이곳에 데이터 클래스를 정의한다.

@Entity
data class Schedule(
   @PrimaryKey val id: Int,
   @NonNull @ColumnInfo(name = "stop_name") val stopName: String,
   @NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int
)
  • id는 기본 키
  • @ColumnInfo로 새 열의 이름을 정한다.
  • Room이 이 클래스를 테이블 정의에 사용할 수 있도록 @Entity를 추가한다.

DAO

DAO는 데이터 액세스 권한을 제공하는 Kotlin 클래스이다.
DAO에는 데이터를 읽고 조작하는 함수가 포함된다.
database.schedule 패키지 아래 ScheduleDao.kt 파일을 추가하고 인터페이스를 정의한다.

@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를 추가해 Room에서 인터페이스를 사용할 수 있도록 한다.
  • 매개변수로 전달된 stopName과 일치하는 결과만 검색하기 위해 WHERE stop_name = :stopName이라고 작성한다.

ViewModel

ScheduleDao를 그대로 사용할 수 있지만 두 개 이상의 화면으로 작업할 경우 뷰에 노출하는 DAO의 일부를 별도의 ViewModel로 분리하는 것이 좋다.

1. ViewModel 클래스

viewmodels패키지 아래 BusScheduleViewModel.kt 파일을 생성한다.

class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {

    fun fullSchedule(): List<Schedule> = scheduleDao.getAll()
    fun scheduleForStopName(name: String): List<Schedule> = scheduleDao.getByStopName(name)
}
  • ScheduleDao 타입의 매개변수를 사용하는 ViewModel 클래스를 만든다.
  • ScheduleDao에 정의된 메서드에 상응하는 메서드를 만든다.

2. ViewModelFactory

뷰 모델을 lifecycle 이벤트에 응답할 수 있는 객체로 인스턴스화 해야한다. 프래그먼트에서 처리하는 대신 인스턴스화에 factory 클래스를 사용할 수 있다.

뷰 모델 클래스 아래 팩토리 클래스를 추가한다.

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")
    }
}

이제 뷰 모델이 lifecycle을 인식할 수 있다.

AppDatabase 클래스

다음 내용을 담고있는 AppDatabase 클래스가 필요하다.

  • 데이터베이스에서 정의되는 항목들
  • 각 DAO 클래스의 단일 인스턴스 액세스 권한 제공
  • 데이터베이스 미리 채우기와 같은 추가 설정

AppDatabase.kt

abstract class AppDatabase: RoomDatabase() {
    abstract fun scheduleDao(): ScheduleDao

    @Database(entities = arrayOf(Schedule::class), version = 1)
    
    companion object{
        @Volatile
        private var INSTANCE: AppDatabase? = null
        fun getDatabase(context: Context): AppDatabase{
            return INSTANCE?: synchronized(this){
                val instance = Room.databaseBuilder(
                    context,
                    AppDatabase::class.java,
                    "app_database"
                )
                    .createFromAsset("database/bus_schedule.db")
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}
  • RoomDatabase에서 상속받는 추상 클래스
  • AppDatabase 클래스를 사용할 때 데이터베이스 인스턴스가 하나만 있는지 확인하기 때문에 companion object를 추가한다.
  • @Volatile은 잠재적 버그를 방지하기 위함?
  • AppDatabase 인스턴스를 반환하는 함수 getDatabase를 정의한다.
  • Elvis 연산자를 사용해 기존 인스턴스를 반환하거나 기존 인스턴스가 없다면 처음으로 데이터베이스를 만든다.

BusScheduleApplication.kt

BusScheduleApplication.kt 파일을 생성하고, Application에서 상속받는 클래스를 생성한다.

class BusScheduleApplication: Application() {
    val database: AppDatabase by lazy { AppDatabase.getDatabase(this) }
}

Manifest에서 application 태그의 android:name 속성을 BusScheduleApplication으로 설정한다.

ListAdapter

이제 모델을 뷰에 연결할 ListAdapter를 작성한다.

지금까지 배운 RecyclerView Adapter는 정적 데이터 목록을 표시했다. 이 방식은 데이터베이스를 사용할 때 데이터 변경사항을 실시간으로 처리할 수 없다. 동적으로 변경되는 목록은 ListAdapter로 처리한다.

ListAdapter는 AsyncListDiffer를 사용하여 이전 데이터 목록과 새 데이터 목록의 차이를 확인한다. 그러면 recycler 뷰가 두 목록 간 차이에 기반해 업데이트를 한다.

1. BusStopAdatper

ListAdapter를 확장하는 클래스를 만든다.

class BusStopAdapter(private val onItemClicked: (Schedule) -> Unit) : ListAdapter<Schedule, BusStopAdapter.BusStopViewHolder>(DiffCallback) {
}

2. BusStopViewHolder 클래스

어댑터 내부에 뷰 홀더가 있어야 레이아웃 파일에서 만들어진 뷰에 액세스할 수 있다.
BusStopViewHolder 클래스를 추가하고 bind()함수를 구현한다.

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)
        )
    }
}

3. onCreateViewHolder 재정의

재정의하고, 레이아웃 inflate하고, onItemClicked()를 호출하도록 onClickLister()를 설정한다.

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
}

4. onBildViewHolder 재정의

재정의해 지정된 position에 뷰를 바인딩한다.

holder.bind(getItem(position))

5. DiffCallback

ListAdapter가 목록을 업데이트할 때 새 목록과 이전 목록에서 어떤 항목이 다른지 확인하는데 필요하다.

  • companion object를 추가해 DiffCallback을 구현한다.
  • 바뀐 항목을 확인하는 arItemTheSame()과 areContentsTheSame() 을 구현한다.
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
       }
   }
}

6. 화면에서 사용하기

어댑터 구현이 끝났고 FullScheduleFragment.kt와 StopScheduleFragment.kt 화면에서 사용하려면 추가적인 구현이 필요하다.

  • 뷰 모델 참조
  • onViewCreated에서 recyclerView의 뷰와 layoutManager 할당
  • 어댑터 속성 할당
  • 목록 업데이트 submitList()

7. 에러 해결

빌드 시 Execution failed for task ':app:kaptDebugKotlin' 에러가 발생했다.
이 링크를 참고하니 맥북 M1에서 발생하는 문제로 kapt "org.xerial:sqlite-jdbc:3.34.0"를 추가하니 해결되었다.

Flow

submitList()가 호출될 때마다 데이터 변경사항을 처리하고 있지만 아직 동적 업데이트를 처리할 수 었다.
⇒ List<Schedule>이 DAO 함수에서 한 번만 반환되기 때문

Flow를 반환하도록 DAO 함수를 수정한다.
List<Schedule> 형태의 반환 유형을 Flow<List<Schedule>>로 수정한다.

0개의 댓글