SW 공부노트

[Android Basics in Kotlin] Data Persistence: Room / Repository / Preference DataStore 본문

안드로이드/안드로이드 Kotlin

[Android Basics in Kotlin] Data Persistence: Room / Repository / Preference DataStore

요빈 2023. 3. 28. 18:27

https://developer.android.com/courses/android-basics-kotlin/unit-5?hl=ko 

 

Kotlin으로 배우는 Android Kotlin 기본사항  |  Kotlin으로 배우는 Android 기본사항 - 데이터 지속성  | 

원활하고 일관된 사용자 환경을 위해 필수 네트워크나 프로세스가 중단되는 경우에도 앱이 계속 작동하도록 합니다.

developer.android.com

해당 글은 위 사이트의 PATHWAY 2 과정인 Use Room for data persistence를 공부하며 작성한 글입니다.

이 과정에서는 앱에 사용자 데이터를 저장하는 방법을 공부합니다.

Room을 사용해 데이터베이스 변경사항을 읽고쓴 뒤 Preference Datastore을 사용해 앱에 사용자 환경설정을 저장하는 법을 배웁니다.


데이터베이스 환경에서는 데이터에 액세스하고 수정하려면 테이블과 쿼리가 있어야한다.

Room에는 다음과 같은 세 가지 주요 구성요소가 있다.

   

   - 데이터 항목: 앱 데이터베이스의 테이블을 말한다.

   - 데이터 액세스 객체(Data Access Object); 앱이 데이터베이스의 데이터를 검색 및 업데이트, 삽입, 삭제하는 데 사용하는 메서드 제공

   - 데이터베이스 클래스: 데이터베이스를 보유하며, 기본 앱 데이터베이스 연결을 위한 기본 액세스 포인트

                                         앱에 데이터베이스와 연결된 DAO 인스턴스를 제공

 

Entity 클래스

 

Entity 클래스는 테이블을 정의하고, 이 클래스의 각 인스턴스는 데이터베이스 테이블의 행을 나타낸다.

데이터 클래스를 통해 Entity 클래스를 정의한다.

@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)

   - @PrimaryKey(autoGenerate = true): autoGenerate 를 통해 Room에서 각 항목의 ID를 생성하도록 함

   - @ColumnInfo: 특정 필드와 연결된 열을 맞춤설정하는 데 사용

      즉, 테이블에서의 name 속성은 데이터 클래스에서 itemName 속성과 연결된다.

 

* 데이터 클래스는 주로 코틀린에서 데이터를 보유하는 데 사용된다. 클래스 앞에 data 키워드를 붙이면 된다.

생성된 코드의 일관성과 의미있는 동작을 보장하기 위해 데이터 클래스는 다음 조건들을 충족해야 된다.

 

   - 기본 생성자에 매개변수가 하나 이상 있어야 함

   - 모든 기본 생성자 매개변수는 val 또는 var로 표시되어야 함

   - 데이터 클래스는 abstract, open, sealed, inner일 수 없다.

 

DAO 

 

DAO(데이터 액세스 객체)는 추상 인터페이스를 제공해 지속성 레이어를 애플리케이션의 나머지 부분과 분리하는 데 사용되는 패턴이다.

DAO의 기능은 애플리케이션 나머지 부분의 기본 지속성 레이어에서 데이터베이스 작업 실행과 관련된 모든 복잡성을 숨기는 것이다.

이를 통해 데이터를 사용하는 코드와 상관없이 데이터 액세스 레이어를 변경할 수 있다.

Room에서 정의하는 DAO는 데이터베이스에 액세스하는 인터페이스를 정의하는 요소이다.

데이터베이스 쿼리/검색, 삽입, 삭제, 업데이트를 위한 편의 메서드를 제공하며 Room은 컴파일 시 이 클래스를 구현한다.

데이터베이스 작업은 실행이 오래 걸릴 수 있으므로 별도의 스레드에서 실행해야 한다.

그렇기 때문에 함수를 suspend 함수로 만들어 코루틴에서 이 함수를 호출할 수 있도록 한다.

 

- @Insert(onConflict = OnConflictStrategy.IGNORE): 충돌이 발생한 경우 IGNORE 전략을 실행해라.

IGNORE 전략은 기본 키가 이미 데이터베이스에 있으면 새 항목을 삽입하지 말고 무시하라는 의미이다.

- FlowLiveData를 반환 유형으로 사용하면 데이터베이스의 데이터가 변경될 때마다 알림을 받을 수 있다.

지속성 레이어에서는 Flow를 사용하는 것이 좋다. Room은 Flow를 자동으로 업데이트하므로 명시적으로 한 번만 데이터를 가져오면 된다.

반환 유형이 Flow일 경우 데이터 변경이 일어났을 경우 업데이트해야 하기 때문에 Room은 백그라운드 스레드에서도 쿼리를 실행한다.

이를 명시적으로 suspend, 코루틴 스콥 내에서 실행할 필요는 없다.

 

데이터베이스 인스턴스

 

데이터베이스 클래스는 개발자가 정의한 DAO의 인스턴스를 앱에 제공한다.

결과적으로 앱은 DAO를 사용하여 데이터베이스의 데이터를 연결된 DAO의 인스턴스로 검색할 수 있다.

@Database 주석을 통해 추상 RoomDatabase 클래스를 만들어준다.

RoomDatabase 인스턴스를 가져오는 일반적인 프로세스는 다음과 같다.

 

   1. RoomDatabase를 확장하는 public abstract 클래스를 만든다. 이 추상 클래스는 데이터베이스 홀더 역할을 한다.

       Room이 구현하기 때문에 추상 클래스로 작성한다.

@Database
abstract class ItemRoomDatabase : RoomDatabase() {}

   

2. @Database 주석을 달고, 데이터베이스 항목을 나열하고, 버전 번호 등을 설정해 Room이 데이터베이스를 빌드할 수 있도록 한다.

@Database(entities = [Item::class], version = 1, exportSchema = false)

      - Item 데이터 클래스의 리스트를 데이터베이스 항목으로 설정한다.

      - 데이터베이스 테이블의 스키마를 변경할 때마다 버전 번호를 높여야 한다.

      - 스키마 버전 기록 백업을 유지하지 않도록 exportSchema를 false로 설정한다.

 

   

3. Dao 인스턴스를 반환하는 추상 메서드나 속성을 정의하면 Room이 구현을 생성한다.

abstract fun itemDao(): ItemDao

   

4. 전체 앱에 RoomDatabase 인스턴스는 하나만 있으면 되므로 RoomDatabase 를 싱글톤으로 만든다.

 

* companion object를 사용해 데이터베이스에 관한 INSTANCE 변수를 선언하고 데이터베이스가 만들어지면 참조를 유지한다. 이를 통해 데이터베이스의 단일 인스턴스를 유지할 수 있다.

 

* INSTANCE 변수는 @Volatile 주석을 달아 휘발성 변수로 선언한다. 휘발성 변수의 값은 캐시되지 않고 모든 쓰기와 읽기가 기본 메모리에서 실행된다. 이렇게 하면 INSTANCE의 값이 항상 최신 상태로 유지되고 모든 실행 스레드에서 같은지 확인할 수 있다.

 

5. Room의 Room.databaseBuilder를 사용해 데이터베이스를 만든다. 기존에 존재했다면 기존 데이터베이스를 반환한다.

 

아래 코드는 companion object 내에 작성한 데이터인스턴스 생성 메서드이다.

// 데이터베이스 인스턴스 생성
// synchronized 통해 한 번에 한 스레드만 접근
fun getDatabase(context: Context):ItemRoomDatabase{
    return INSTANCE ?: synchronized(this){
        // 데이터베이스 빌더를 사용 -> 빌더에 인수로 context, 데이터베이스 클래스, 데이터베이스 이름을 전달
        val instance = Room.databaseBuilder(
            context.applicationContext,
            ItemRoomDatabase::class.java,
            "item_database")
            .fallbackToDestructiveMigration() 
            .build()
        INSTANCE = instance
        return instance
    }
}

* 여러 스레드가 경합 상태로 실행돼 동시에 데이터베이스를 요청하여 데이터베이스가 여러개 생성되는 것을 막기 위해 코드를 래핑해 synchronized 블록 내에서 실행한다. 이렇게 하면 한 번에 한 스레드만 이 코드 블록에 들어갈 수 있으므로 데이터베이스가 한 번만 초기화된다.

 

* 일반적으로 스키마 변경 시점에 관한 이전 전략과 함께 이전 객체를 제공해야 한다. 이전 객체는 데이터가 손실되지 않도록 이전 스키마의 모든 행을 가져와 새 스키마의 행으로 변환하는 방법을 정의하는 객체이다. 간단한 해결방법은 데이터베이스를 제거했다가 다시 빌드하는 것인데, 한 마디로 데이터가 손실된다는 의미이다.

 

* Application 클래스 구현

Application 클래스에서 데이터베이스 인스턴스를 인스턴스화해 앱 전체에서 사용할 수 있도록 한다.

 

ViewModel

 

ViewModel은 DAO를 통해 데이터베이스와 상호작용하여 UI에 데이터를 제공한다. 

모든 데이터베이스 작업은 기본 UI 스레드에서 벗어나 실행되어야 하고, 코루틴과 viewModelScope을 사용하면 된다.

 

* ItemDao 객체를 매개변수로 기본 생성자에 전달 -> Dao 내 메소드통해 데이터베이스와 상호작용하기 위해

 

* ViewModelProvider.Factory 통해 ViewModelFactory 생성해 뷰 모델 인스턴스화할 때 사용

create() 메소드 재정의해 modelClass가 InventoryViewModel 클래스와 같으면 그 인스턴스를 반환하고 같지 않으면 예외가 발생한다.


확장 함수(Extension functions)

 

코틀린은 클래스에서 상속받거나 기존 클래스 정의를 수정하지 않고도 클래스를 확장하는 기능을 제공한다.

즉, 소스 코드에 액세스하지 않고도 함수를 기존 클래스에 추가할 수 있다.

확장 함수수정할 수 없는 타사 라이브버리 클래스에 함수를 추가할 때 많이 사용한다.

확장 함수는 다음과 같은 형식으로 정의할 수 있다.

 fun 클래스이름.함수이름(인수): 리턴타입 { 구현부 }
 
 fun Square.area(): Double {
 	return 0.0
}

 

ListAdapter

 

ListAdapter를 사용하면 recycler 뷰가 두 목록 간 차이에 기반해서만 업데이트 된다.

그 결과 recycler 뷰가 자주 업데이트되는 데이터를 처리할 때 더 나은 성능을 발휘한다.

 

RecyclerView에 결과 출력

 

- LiveData인 allItems을 observe해 변경사항이 있을 경우 반영되도록 한다.

viewModel.allItems.observe(this.viewLifecycleOwner){
        items -> items.let { adapter.submitList(it) }
}

viewModel.retrieveItem(id).observe(this.viewLifecycleOwner){ selectedItem ->
            item = selectedItem
            bind(item)
}

LiveData 다룰 때는 observe{ new -> new 변수 다루기 } 형식으로 작성한다.

 

- 아이템 클릭하면 세부항목으로 넘어갈 수 있도록 adpater 인수 통해 클릭 리스너 전달

val adapter = ItemListAdapter{
    val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
    findNavController().navigate(action)
}

 

데이터 업데이트

 

- 데이터 클래스: copy()

 

copy() 함수는 기본적으로 데이터 클래스의 모든 인스턴스에 제공된다.

이 함수는 일부 속성을 변경하지만 나머지 속성은 변경하지 않고 그대로 두기 위한 객체 복사에 사용된다.

copy() 함수는 다음과 같이 사용된다.

val newItem = item.copy(quantityInStock = item.quantityInStock-1)

새로 배운 것

 

- Flow 데이터를 asLiveData()를 통해 LiveData값으로 사용 -> 변경사항 반영 가능

 

- 데이터 수정 시 데이터 추가 레이아웃을 재사용

item 식별하는 id를 인수로 줘 id에 맞는 item 호출해 출력

 

- 화면 간 데이터 전달할 때 intent 대신 navigate 사용

이동 전 화면에서 action 변수에 인수를 넣어 보내면 이동 후 화면에서 아래 코드 통해 받음

val navigationArgs: ItemDetailFragmentArgs by navArgs()

 

- navigation을 통해 화면을 이동할 수 있는 방식은 2가지

 

   (1) 탐색 아이디를 직접 전달

findNavController().navigate(R.id.action_addItemFragment_to_itemListFragment)

   (2) action 변수를 만들어(인수 포함 가능) 변수 상태로 전달

* Directions 클래스 사용 -> navigate 사용하면 자동 생성되는 클래스

val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)

앱이 실행될 때마다 서버에서 데이터를 가져오고 사용자에게 로드 화면이 표시되면 UX에 부정적인 영향을 미칠 수 있다.

오프라인 캐싱을 구현하면 앱 실행 시 데이터가 더 빠르게 표시된다.

오프라인 캐싱은 앱이 네트워크에서 가져온 데이터를 기기의 로컬 저장소에 저장한다는 의미로, 액세스 속도가 더 빨라진다.

이를 위해선 앱 데이터의 단일 정보 소스 역할을 하고 데이터 소스(네트워크, 캐시 등)을 뷰 모델에서 분리하는 저장소 클래스를 구현한다.

 

저장소 패턴(Repository pattern)

저장소 패턴데이터 레이어를 앱의 나머지 부분에서 분리하는 디자인 패턴이다.

데이터 레이어는 UI와는 별도로 앱의 데이터와 비즈니스 로직을 처리하는 앱 부분을 나타내면 나머지 앱에서 이 데이터에 액세스할 수 있도록 일관된 API를 사용한다. UI가 사용자에게 정보를 제공하는 동안 데이터 레이어에는 네트워킹 코드, Room 데이터베이스, 오류 처리, 데이터를 읽거나 조작하는 코드 등이 포함된다.

저장소는 데이터 소스(데이터 모델, 웹 서비스, 캐시)간의 충돌을 해결하고 이 데이터의 변경사항을 중앙 집중화할 수 있다.

아래 다이어그램의 액티비티와 같은 앱 구성요소가 리포지토리를 통해 데이터 소스와 상호작용하는 방법을 보여준다.

저장소를 구현하려면 Repository 클래스를 별도로 생성한다.

Repository 클래스는 앱의 나머지 부분에서 데이터 소스를 분리하고 앱의 나머지 부분의 데이터 액세스를 위한 깔끔한 API를 제공한다.

 

저장소 사용의 이점

 

저장소 모듈은 데이터 작업을 처리하고 여러 백엔드 사용을 허용한다. 일반적인 실제 앱에서 저장소는 네트워크에서 데이터를 가져올지 로컬 데이터베이스에서 캐시된 결과를 사용할지 결정하는 로직을 구현한다.

저장소를 사용하면 뷰 모델과 같은 호출 코드에 영향을 주지 않고 다른 지속성 라이브러리로의 이전과 같은 구현 세부정보를 교체할 수 있다. 이는 코드를 모듈식으로, 테스트 가능하게 만드는데도 도움이 된다. 

 

저장소는 앱 데이터의 특정 부분에 대한 단일 정보 소스 역할을 해야한다.

네트워크 리소스와 오프라인 캐시 등 여러 데이터소스로 작업할 때 저장소는 앱의 데이터가 최대한 정확하고 최신 상태로 유지되도록 하므로 앱이 오프라인 상태일 때도 최상의 환경을 제공한다. 

 

캐싱

 

캐시앱에서 사용하는 데이터 저장소를 말한다. 예를 들어 인터넷 연결이 끊기는 경우 네트워크의 데이터를 일시적으로 저장할 수 있다.

네트워크를 더 이상 사용할 수 없더라도 앱은 여전히 캐시된 데이터로 대체할 수 있다.

캐시는 더 이상 화면에 표시되지 않는 활동의 임시 데이터를 저장하거나 앱 실행 사이에 데이터를 유지하는 데도 유용하다.

 

다음 표는 Android에서 네트워크 캐싱을 구현하는 여러 가지 방법들이다.

Room을 사용하는 방법은 기기 파일 시스템에 구조화된 데이터를 저장하는 데 권장되는 방법이다.


이번 예제의 목표는 저장소 패턴을 사용해 오프라인 캐싱을 구현하여 데이터 레이어를 UI 코드에서 분리하는 것이다.

Room 데이터베이스를 통해 캐싱 기능을 구현하고 저장소(repository)를 사용할 수 있도록 뷰 모델을 업데이트한다.

 

1. 오프라인 캐시를 관리할 저장소(Repository) 생성

 

캐싱을 위한 저장소 패턴을 구현하는 VideosRepository 클래스를 생성한다.

VideosDatabase객체를 리포지토리 클래스 생성자 매개변수로 전달해 DAO 메서드에 액세스한다.

class VideosRepository(private val database: VideosDatabase) {
}

 

* Android의 데이터베이스는 파일 시스템이나 디스크에 저장되며 저장하려면 디스크 I/O 작업을 해야한다.

디스크 I/O 작업은 속도가 느리며 작업이 완료될 때까지 항상 현재 스레드가 차단된다.

따라서 I/O 디스패처를 통해 디스크 I/O 를 실행해야 한다. 아래 코드의 withContext ~ 를 통해 디스패처를 사용할 수 있다.

이 디스패처는 차단 I/O 작업을 공유 스레드풀로 오프로드하도록 설계되었다.

쉽게 말해 코루틴 컨텍스트를 Dispatcher.IO로 전환하여 네트워크 및 데이터베이스 작업을 실행한다.

suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
   }
}

 

2. 데이터베이스에서 데이터 검색

 

데이터베이스에서 동영상 재생목록 데이터를 불러와 LiveData 객체에 저장한다.

하지만 Dao를 통해 불러온 데이터는 DatabaseVideo이므로 DevByteVideo로 변환해줘야 한다.

Transformation.map() 메서드를 통해 하나의 LiveData 객체를 또 다른 LiveData 객체로 변환한다.

val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()){
	it.asDomainModel()
}

 

3. ViewModel에서 Repository 사용

 

데이터베이스 새로고침은 네트워크의 데이터와 동기화를 유지하기 위해 로컬 데이터베이스를 업데이트하거나 새로고침하는 프로세스이다.

또한 이 새로고침 전략을 통해 ViewModel에서 네트워크를 통해 직접 데이터를 가져오지 않고 저장소를 사용하게 된다.

 

결과적으로 데이터는 NetworkVideo -> DatabaseVideo -> DevByteVideo 순서로 변환된다.

NetworkVideo는 Retrofit에서 불러온 데이터이다.

네트워크로부터 불러온 데이터를 DatabaseVideo로 변환해 데이터베이스에 insert 한다.

viewModel에서 데이터에 접근할 때는 repository에 선언한 videos 변수를 사용하는 데,

이 변수는 데이터베이스에서 불러온 데이터를 DevByteVideo로 변환한 변수이다.


Preference DataStore

 

Jetpack DataStore 라이브러리를 사용하면 데이터 저장을 위한 간단하고 안전한 비동기 API를 만들 수 있다.

Jetpack DataStorePreferences DataStoreProto DataStore라는 두 가지 구현을 제공한다.

Preferences DataStore와 Proto DataStore에서는 모두 데이터 저장이 가능하지만 저장 방법이 다르다.

 

   - Preferences DataStore는 먼저 스키마(데이터베이스 모델)를 정의하지 않고 키에 기반하여 데이터에 액세스하고 저장한다.

 

   - Proto DataStore는 프로토콜 버퍼를 사용해 스키마를 정의한다. 프로토콜 버퍼를 사용하면 강타입 데이터를 유지할 수 있다.

 

Room vs Datastore

 

애플리케이션에서 대용량/복잡한 데이터를 SQL과 같은 구조화된 형식으로 저장해야 한다면 Room을 사용하는 것이 좋다.

하지만 키-값 쌍으로 저장할 수 있는 간단하거나 적은 양의 데이터라면 DataStore가 적합하다.

 

Preferences DataStore vs Proto DataStore

 

Proto DataStore는 유형에 안전하고 효율적이지만 구성과 설정이 필요하다. 앱 데이터가 키-값 쌍으로 저장할 수 있을 만큼 간단하면 Preferences DataStore가 훨씬 쉽게 설정할 수 있기 때문에 더 적합하다.

 

Preferences DataStore 사용 방법

1. Preferences DataStore를 종속 항목으로 추가

 

build.gradle(module)에 다음 종속 항목을 추가한다.

implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

 

2. Preference DataStore 인스턴스 만들기

 

preferencesDataStore 위임을 사용해 DataStore 인스턴스를 만든다.

Preference를 사용하고 있으므로 DataStore 유형으로 Preference를 전달하고 name 속성도 설정해준다.

private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCES_NAME
)

* Kotlin 파일의 최상위 수준에서 DataStore를 한 번 만들면 애플리케이션의 나머지 부분에서 이 속성을 통해 인스턴스에 액세스할 수 있다. 이렇게 하면 더 간편하게 DataStore를 싱글톤으로 유지할 수 있다.

 

3. DataStore 클래스 만들기

 

이 단계에서는 설정을 저장하는 데 필요한 키를 정의해 Preference DataStore에 쓰고 읽는 함수를 정의한다.

 

   - 키 설정

DataStore<Preferences>에 키를 정의하려면 키 유형 함수를 사용하면 된다.

예를 들어 특정 자료형(int, String)의 키를 정의하려면 intPreferencesKey() / StringPreferencesKey()를 사용하면 된다.

private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")

 

   - 값 설정

키를 사용해 레이아웃 설정을 DataStore에 저장해보자.

Preference DataStore는 DataStore의 데이터를 업데이트하는 edit()이라는 suspend 함수를 제공한다.

변환 블록의 모든 코드는 단일 트랜잭션으로 취급된다.

내부적으로 트랜잭션 작업은 Dispatcher.IO로 이동하므로 함수를 suspend로 선언해야 한다.

suspend fun savePreferencesDataStore(isLinear: Boolean, context: Context){
	context.dataStore.edit { preferences ->
    	preferences[IS_LINEAR] = isLinear
    }
}

 

   - 값 읽기

Preference DataStore는 환경설정이 변경될 때마다 Flow<Preferences>를 내보낸다.

전체 Preference 객체를 노출하지 않고 사용할 값만 사용하는 것이 좋다.

이를 위해  Flow<Preferences>를 매핑하고 관심 있는 Boolean값을 가져오면 된다.

val preferenceFlow: Flow<Boolean> = context.dataStore.data.map{ preferences ->
	preferencs[IS_LINEAR] ?: true
}

* DataStore는 파일에서 데이터를 읽고 쓰므로 데이터에 엑세스할 때 IOException이 발생할 수 있다.

따라서 catch() 연산자를 통해 예외를 포착해 문제를 처리해야 한다.

 

4. DataStore 사용

 

일단 클래스 변수를 선언한 후 onViewCreated()에서 변수를 초기화한다.

private lateinit var SettingsDataStore: SettingsDataStore

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   SettingsDataStore = SettingsDataStore(requireContext())
}

   

   - 데이터 읽기 및 관찰

preferenceFlow를 LiveData로 변환한 후 관찰자를 연결해 데이터를 관찰한다.

관찰자 내에서 새 설정값을 isLinearLayoutManager에 할당하고 chooseLayout 함수를 통해 레이아웃을 업데이트한다.

SettingsDataStore.preferenceFlow.asLiveData.observe(viewLifecycleOwner){ value ->
	isLinearLayoutManager = value
	chooseLayout()
}

 

   - DataStore에 레이아웃 설정 쓰기

사용자가 레이아웃 설정을 변경하면 변화를 감지해 알아서 설정값을 바꿔야 한다.

Preference DataStore에 데이터를 쓰려면 코루틴 내에서 비동기적으로 실행되어야 한다.

프래그먼트 내에서 이 작업을 실행하려면 LifecycleScope이라는 코루틴 스콥을 사용해야 한다.

LifecycleScope은 각 Lifecycle 객체에서 정의되며 이 스콥에서 실행된 코루틴은 Lifecycle 소유자가 소멸될 때 취소된다.

lifecycleScope.launch{
	SettingDataStore.saveLayoutToPreferencesStore(isLinearLayoutManager, requireContext())
}

 

  +) 메뉴 아이콘 알맞게 수정

메뉴가 만들어지면 프레임마다  같은 메뉴를 다시 그리는 것이 중복되기 때문에 프레임마다 다시 그리지 않는다.

invalidateOptionsMenu() 함수는 Android에 옵션 메뉴를 다시 그리라고 지시한다.

메뉴 항목 추가나 항목 삭제, 메뉴 텍스트나 아이콘 변경과 같이 옵션 메뉴에서 무언가 변경할 때 이 함수를 호출할 수 있다.