DataStore를 사용하여 로컬에 환경설정 저장

1. 시작하기 전에

소개

이 단원에서는 SQL과 Room을 사용하여 기기에 로컬로 데이터를 저장하는 방법을 알아봤습니다. SQL과 Room은 강력한 도구입니다. 그러나 관계형 데이터를 저장할 필요가 없는 경우 DataStore가 간단한 솔루션이 될 수 있습니다. DataStore Jetpack 구성요소는 오버헤드가 낮은 작고 간단한 데이터 세트를 저장하는 좋은 방법입니다. DataStore에는 서로 다른 두 가지 구현(Preferences DataStore, Proto DataStore)이 있습니다.

  • Preferences DataStore는 키-값 쌍을 저장합니다. 값은 String, Boolean, Integer와 같은 Kotlin의 기본 데이터 유형일 수 있습니다. 복잡한 데이터 세트는 저장하지 않습니다. 사전 정의된 스키마도 필요하지 않습니다. Preferences Datastore의 기본 사용 사례는 사용자 환경설정을 기기에 저장하는 것입니다.
  • Proto DataStore는 맞춤 데이터 유형을 저장합니다. proto 정의를 객체 구조로 매핑하는 사전 정의된 스키마가 필요합니다.

이 Codelab에서는 Preferences DataStore만 다루며 DataStore 문서에서 Proto DataStore를 자세히 알아볼 수 있습니다.

Preferences DataStore는 사용자 제어 설정을 저장하는 좋은 방법입니다. 이 Codelab에서는 DataStore를 구현하여 정확히 이를 실행하는 방법을 알아봅니다.

기본 요건

필요한 항목

  • 인터넷 액세스가 가능하고 Android 스튜디오가 설치된 컴퓨터
  • 기기 또는 에뮬레이터
  • Dessert Release 앱의 시작 코드

빌드할 항목

Dessert Release 앱에서 Android 출시 목록을 보여줍니다. 앱 바 아이콘의 레이아웃이 그리드로 보기와 목록 보기 간에 전환합니다.

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

현재 상태에서는 앱이 레이아웃 선택을 유지하지 않습니다. 앱을 닫으면 레이아웃 선택이 저장되지 않고 설정이 기본 선택으로 돌아갑니다. 이 Codelab에서는 DataStore를 Dessert Release 앱에 추가하고 이를 사용하여 레이아웃 선택 환경설정을 저장합니다.

2. 시작 코드 다운로드

다음 링크를 클릭하면 이 Codelab의 모든 코드를 다운로드할 수 있습니다.

또는 원한다면 GitHub에서 Dessert Release 코드를 클론할 수도 있습니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git
$ cd basic-android-kotlin-compose-training-dessert-release
$ git checkout starter
  1. Android 스튜디오에서 basic-android-kotlin-compose-training-dessert-release 폴더를 엽니다.
  2. Android 스튜디오에서 Dessert Release 앱 코드를 엽니다.

3. 종속 항목 설정

app/build.gradle.kts 파일의 dependencies에 다음을 추가합니다.

implementation("androidx.datastore:datastore-preferences:1.0.0")

4. 사용자 환경설정 저장소 구현

  1. data 패키지에서 UserPreferencesRepository라는 새 클래스를 만듭니다.

c4c2e90902898001.png

  1. UserPreferencesRepository 생성자에서 비공개 값 속성을 정의하여 Preferences 유형으로 DataStore 객체 인스턴스를 나타냅니다.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
}

DataStore는 키-값 쌍을 저장합니다. 값에 액세스하려면 키를 정의해야 합니다.

  1. UserPreferencesRepository 클래스 내에 companion object를 만듭니다.
  2. booleanPreferencesKey() 함수를 사용하여 키를 정의하고 is_linear_layout이라는 이름을 전달합니다. SQL 테이블 이름과 마찬가지로 키도 밑줄 형식을 사용해야 합니다. 이 키는 선형 레이아웃의 표시 여부를 나타내는 불리언 값에 액세스하는 데 사용됩니다.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
    private companion object {
        val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    }
    ...
}

DataStore에 쓰기

edit() 메서드에 람다를 전달하여 DataStore 내에서 값을 만들고 수정합니다. 람다에는 DataStore의 값을 업데이트하는 데 사용할 수 있는 MutablePreferences 인스턴스가 전달됩니다. 이 람다 내의 모든 업데이트는 단일 트랜잭션으로 실행됩니다. 즉, 업데이트가 원자적으로 이루어져 한 번에 모두 실행됩니다. 이 유형의 업데이트는 일부 값은 업데이트되고 다른 값은 업데이트되지 않는 상황을 방지합니다.

  1. 정지 함수를 만들고 이름을 saveLayoutPreference()로 지정합니다.
  2. saveLayoutPreference() 함수에서 dataStore 객체의 edit() 메서드를 호출합니다.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit {

    }
}
  1. 코드를 더 쉽게 읽을 수 있도록 람다 본문에 제공된 MutablePreferences의 이름을 정의합니다. 이 속성을 사용하여, 정의한 키와 saveLayoutPreference() 함수에 전달된 부울로 값을 설정합니다.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit { preferences ->
        preferences[IS_LINEAR_LAYOUT] = isLinearLayout
    }
}

DataStore에서 읽기

이제 isLinearLayoutdataStore에 쓰는 방법을 만들었으므로 다음 단계를 따라 이를 읽습니다.

  1. UserPreferencesRepositoryFlow<Boolean> 유형의 isLinearLayout이라는 속성을 만듭니다.
val isLinearLayout: Flow<Boolean> =
  1. DataStore.data 속성을 사용하여 DataStore 값을 노출할 수 있습니다. isLinearLayoutDataStore 객체의 data 속성으로 설정합니다.
val isLinearLayout: Flow<Boolean> = dataStore.data

data 속성은 Preferences 객체의 Flow입니다. Preferences 객체에는 DataStore의 모든 키-값 쌍이 포함되어 있습니다. DataStore의 데이터가 업데이트될 때마다 새 Preferences 객체가 Flow로 내보내집니다.

  1. map 함수를 사용하여 Flow<Preferences>Flow<Boolean>으로 변환합니다.

이 함수는 현재 Preferences 객체가 포함된 람다를 매개변수로 허용합니다. 이전에 정의한 키를 지정하여 레이아웃 환경설정을 가져올 수 있습니다. 값은 saveLayoutPreference가 아직 호출되지 않은 경우 존재하지 않을 수 있으므로 기본값도 제공해야 합니다.

  1. 선형 레이아웃 뷰가 기본값이 되도록 true를 지정합니다.
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
    preferences[IS_LINEAR_LAYOUT] ?: true
}

예외 처리

기기에서 파일 시스템과 상호작용할 때 문제가 발생할 수 있습니다. 예를 들어 파일이 존재하지 않거나 디스크가 꽉 찼거나 마운트 해제되었을 수 있습니다. DataStore는 파일에서 데이터를 읽고 쓰므로 DataStore에 액세스할 때 IOExceptions가 발생할 수 있습니다. catch{} 연산자를 사용하여 예외를 포착하고 이러한 실패를 처리합니다.

  1. 컴패니언 객체에서 로깅에 사용할 변경 불가능한 TAG 문자열 속성을 구현합니다.
private companion object {
    val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    const val TAG = "UserPreferencesRepo"
}
  1. 데이터를 읽는 동안 오류가 발생하면 Preferences DataStore에서 IOException이 발생합니다. isLinearLayout 초기화 블록에서 map() 앞에 catch{} 연산자를 사용하여 IOException을 포착합니다.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {}
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }
  1. catch 블록에 IOexception이 있으면 오류를 기록하고 emptyPreferences()를 내보냅니다. 다른 유형의 예외가 발생한다면 해당 예외를 다시 발생시키는 편이 좋습니다. 오류가 있는 경우 emptyPreferences()를 내보내면 map 함수가 여전히 기본값에 매핑될 수 있습니다.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {
        if(it is IOException) {
            Log.e(TAG, "Error reading preferences.", it)
            emit(emptyPreferences())
        } else {
            throw it
        }
    }
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }

5. DataStore 초기화

이 Codelab에서는 종속 항목 삽입을 수동으로 처리해야 합니다. 따라서 UserPreferencesRepository 클래스에 Preferences DataStore를 수동으로 제공해야 합니다. 다음 단계를 따라 DataStoreUserPreferencesRepository에 삽입하세요.

  1. dessertrelease 패키지를 찾습니다.
  2. 이 디렉터리 내에서 새 클래스 DessertReleaseApplication을 만들고 Application 클래스를 구현합니다. 이 클래스는 DataStore의 컨테이너입니다.
class DessertReleaseApplication: Application() {
}
  1. DessertReleaseApplication.kt 파일 내 그리고 DessertReleaseApplication 클래스 외부에서 LAYOUT_PREFERENCE_NAME이라는 private const val을 선언합니다.
  2. LAYOUT_PREFERENCE_NAME 변수에 문자열 값 layout_preferences를 할당합니다. 이 값은 다음 단계에서 인스턴스화하는 Preferences Datastore의 이름으로 사용할 수 있습니다.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
  1. 계속 DessertReleaseApplication 클래스 본문 외부 그리고 DessertReleaseApplication.kt 파일 내에서 preferencesDataStore 위임을 사용하여 DataStore<Preferences> 유형의 비공개 값 속성 Context.dataStore를 만듭니다. preferencesDataStore 위임의 name 매개변수에 LAYOUT_PREFERENCE_NAME을 전달합니다.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)
  1. DessertReleaseApplication 클래스 본문 내에서 UserPreferencesRepositorylateinit var 인스턴스를 만듭니다.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository
}
  1. onCreate() 메서드를 재정의합니다.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository

    override fun onCreate() {
        super.onCreate()
    }
}
  1. onCreate() 메서드 내에서 dataStore를 매개변수로 사용하여 UserPreferencesRepository를 구성해 userPreferencesRepository를 초기화합니다.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository

    override fun onCreate() {
        super.onCreate()
        userPreferencesRepository = UserPreferencesRepository(dataStore)
    }
}
  1. 다음 줄을 AndroidManifest.xml 파일의 <application> 태그 내에 추가합니다.
<application
    android:name=".DessertReleaseApplication"
    ...
</application>

이 접근 방식은 DessertReleaseApplication 클래스를 앱의 진입점으로 정의합니다. 이 코드의 목적은 MainActivity를 실행하기 전에 DessertReleaseApplication 클래스에 정의된 종속 항목을 초기화하는 것입니다.

6. UserPreferencesRepository 사용

ViewModel에 저장소 제공

이제 종속 항목 삽입을 통해 UserPreferencesRepository를 사용할 수 있으므로 DessertReleaseViewModel에서 이를 사용할 수 있습니다.

  1. DessertReleaseViewModel에서 UserPreferencesRepository 속성을 생성자 매개변수로 만듭니다.
class DessertReleaseViewModel(
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    ...
}
  1. ViewModel의 컴패니언 객체 내 viewModelFactory initializer 블록에서 다음 코드를 사용하여 DessertReleaseApplication의 인스턴스를 가져옵니다.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                ...
            }
        }
    }
}
  1. DessertReleaseViewModel의 인스턴스를 만들고 userPreferencesRepository를 전달합니다.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                DessertReleaseViewModel(application.userPreferencesRepository)
            }
        }
    }
}

이제 ViewModel에서 UserPreferencesRepository에 액세스할 수 있습니다. 다음 단계에서는 이전에 구현한 UserPreferencesRepository의 읽기 및 쓰기 기능을 사용합니다.

레이아웃 환경설정 저장

  1. DessertReleaseViewModel에서 selectLayout() 함수를 수정하여 환경설정 저장소에 액세스하고 레이아웃 환경설정을 업데이트합니다.
  2. DataStore에 쓰는 작업은 suspend 함수를 사용하여 비동기식으로 실행됩니다. 새 코루틴을 시작하여 환경설정 저장소의 saveLayoutPreference() 함수를 호출합니다.
fun selectLayout(isLinearLayout: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.saveLayoutPreference(isLinearLayout)
    }
}

레이아웃 환경설정 읽기

이 섹션에서는 ViewModel의 기존 uiState: StateFlow를 리팩터링하여 저장소의 isLinearLayout: Flow를 반영합니다.

  1. uiState 속성을 MutableStateFlow(DessertReleaseUiState)로 초기화하는 코드를 삭제합니다.
val uiState: StateFlow<DessertReleaseUiState> =

저장소의 선형 레이아웃 환경설정에는 true 또는 false라는 가능한 두 값이 Flow<Boolean> 형식으로 있습니다. 이 값은 UI 상태에 매핑되어야 합니다.

  1. StateFlowisLinearLayout Flow에서 호출된 map() 컬렉션 변환의 결과로 설정합니다.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
  1. DessertReleaseUiState 데이터 클래스의 인스턴스를 반환하고 isLinearLayout Boolean을 전달합니다. 화면에서는 이 UI 상태를 사용하여 표시할 올바른 문자열과 아이콘을 결정합니다.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }

UserPreferencesRepository.isLinearLayout콜드 Flow입니다. 그러나 UI에 상태를 제공하려면 StateFlow와 같은 핫 흐름을 사용하는 것이 좋습니다. 그러면 UI에서 항상 상태를 즉시 사용할 수 있습니다.

  1. stateIn() 함수를 사용하여 FlowStateFlow로 변환합니다.
  2. stateIn() 함수는 세 가지 매개변수(scope, started, initialValue)를 허용합니다. 이러한 매개변수에 각각 viewModelScope, SharingStarted.WhileSubscribed(5_000), DessertReleaseUiState()를 전달합니다.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }
.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = DessertReleaseUiState()
    )
  1. 앱을 실행합니다. 전환 아이콘을 클릭하면 그리드 레이아웃과 선형 레이아웃 간에 전환되는 것을 확인할 수 있습니다.

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

축하합니다 Preferences DataStore를 앱에 추가하여 사용자의 레이아웃 환경설정을 저장했습니다.

7. 솔루션 코드 가져오기

완료된 Codelab의 코드를 다운로드하려면 다음 git 명령어를 사용하면 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git
$ cd basic-android-kotlin-compose-training-dessert-release
$ git checkout main

또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

솔루션 코드를 보려면 GitHub에서 확인하세요.