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를 구현하여 정확히 이를 실행하는 방법을 알아봅니다.
기본 요건
- Room을 사용하여 데이터 읽기 및 업데이트 Codelab을 통해 'Compose 사용 시 알아야 하는 Android 기본사항' 과정을 완료합니다.
필요한 항목
- 인터넷 액세스가 가능하고 Android 스튜디오가 설치된 컴퓨터
- 기기 또는 에뮬레이터
- Dessert Release 앱의 시작 코드
빌드할 항목
Dessert Release 앱에서 Android 출시 목록을 보여줍니다. 앱 바 아이콘의 레이아웃이 그리드로 보기와 목록 보기 간에 전환합니다.
 
 
현재 상태에서는 앱이 레이아웃 선택을 유지하지 않습니다. 앱을 닫으면 레이아웃 선택이 저장되지 않고 설정이 기본 선택으로 돌아갑니다. 이 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
- Android 스튜디오에서 basic-android-kotlin-compose-training-dessert-release폴더를 엽니다.
- Android 스튜디오에서 Dessert Release 앱 코드를 엽니다.
3. 종속 항목 설정
app/build.gradle.kts 파일의 dependencies에 다음을 추가합니다.
implementation("androidx.datastore:datastore-preferences:1.0.0")
4. 사용자 환경설정 저장소 구현
- data패키지에서- UserPreferencesRepository라는 새 클래스를 만듭니다.

- UserPreferencesRepository생성자에서 비공개 값 속성을 정의하여- Preferences유형으로- DataStore객체 인스턴스를 나타냅니다.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
}
DataStore는 키-값 쌍을 저장합니다. 값에 액세스하려면 키를 정의해야 합니다.
- UserPreferencesRepository클래스 내에- companion object를 만듭니다.
- 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 인스턴스가 전달됩니다. 이 람다 내의 모든 업데이트는 단일 트랜잭션으로 실행됩니다. 즉, 업데이트가 원자적으로 이루어져 한 번에 모두 실행됩니다. 이 유형의 업데이트는 일부 값은 업데이트되고 다른 값은 업데이트되지 않는 상황을 방지합니다.
- 정지 함수를 만들고 이름을 saveLayoutPreference()로 지정합니다.
- saveLayoutPreference()함수에서- dataStore객체의- edit()메서드를 호출합니다.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit {
    }
}
- 코드를 더 쉽게 읽을 수 있도록 람다 본문에 제공된 MutablePreferences의 이름을 정의합니다. 이 속성을 사용하여, 정의한 키와saveLayoutPreference()함수에 전달된 부울로 값을 설정합니다.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit { preferences ->
        preferences[IS_LINEAR_LAYOUT] = isLinearLayout
    }
}
DataStore에서 읽기
이제 isLinearLayout을 dataStore에 쓰는 방법을 만들었으므로 다음 단계를 따라 이를 읽습니다.
- UserPreferencesRepository에- Flow<Boolean>유형의- isLinearLayout이라는 속성을 만듭니다.
val isLinearLayout: Flow<Boolean> =
- DataStore.data속성을 사용하여- DataStore값을 노출할 수 있습니다.- isLinearLayout을- DataStore객체의- data속성으로 설정합니다.
val isLinearLayout: Flow<Boolean> = dataStore.data
data 속성은 Preferences 객체의 Flow입니다. Preferences 객체에는 DataStore의 모든 키-값 쌍이 포함되어 있습니다. DataStore의 데이터가 업데이트될 때마다 새 Preferences 객체가 Flow로 내보내집니다.
- map 함수를 사용하여 Flow<Preferences>를Flow<Boolean>으로 변환합니다.
이 함수는 현재 Preferences 객체가 포함된 람다를 매개변수로 허용합니다. 이전에 정의한 키를 지정하여 레이아웃 환경설정을 가져올 수 있습니다. 값은 saveLayoutPreference가 아직 호출되지 않은 경우 존재하지 않을 수 있으므로 기본값도 제공해야 합니다.
- 선형 레이아웃 뷰가 기본값이 되도록 true를 지정합니다.
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
    preferences[IS_LINEAR_LAYOUT] ?: true
}
예외 처리
기기에서 파일 시스템과 상호작용할 때 문제가 발생할 수 있습니다. 예를 들어 파일이 존재하지 않거나 디스크가 꽉 찼거나 마운트 해제되었을 수 있습니다. DataStore는 파일에서 데이터를 읽고 쓰므로 DataStore에 액세스할 때 IOExceptions가 발생할 수 있습니다. catch{} 연산자를 사용하여 예외를 포착하고 이러한 실패를 처리합니다.
- 컴패니언 객체에서 로깅에 사용할 변경 불가능한 TAG문자열 속성을 구현합니다.
private companion object {
    val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    const val TAG = "UserPreferencesRepo"
}
- 데이터를 읽는 동안 오류가 발생하면 Preferences DataStore에서IOException이 발생합니다.isLinearLayout초기화 블록에서map()앞에catch{}연산자를 사용하여IOException을 포착합니다.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {}
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }
- 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를 수동으로 제공해야 합니다. 다음 단계를 따라 DataStore를 UserPreferencesRepository에 삽입하세요.
- dessertrelease패키지를 찾습니다.
- 이 디렉터리 내에서 새 클래스 DessertReleaseApplication을 만들고Application클래스를 구현합니다. 이 클래스는 DataStore의 컨테이너입니다.
class DessertReleaseApplication: Application() {
}
- DessertReleaseApplication.kt파일 내 그리고- DessertReleaseApplication클래스 외부에서- LAYOUT_PREFERENCE_NAME이라는- private const val을 선언합니다.
- LAYOUT_PREFERENCE_NAME변수에 문자열 값- layout_preferences를 할당합니다. 이 값은 다음 단계에서 인스턴스화하는- Preferences Datastore의 이름으로 사용할 수 있습니다.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
- 계속 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
)
- DessertReleaseApplication클래스 본문 내에서- UserPreferencesRepository의- lateinit 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
}
- 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()
    }
}
- 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)
    }
}
- 다음 줄을 AndroidManifest.xml파일의<application>태그 내에 추가합니다.
<application
    android:name=".DessertReleaseApplication"
    ...
</application>
이 접근 방식은 DessertReleaseApplication 클래스를 앱의 진입점으로 정의합니다. 이 코드의 목적은 MainActivity를 실행하기 전에 DessertReleaseApplication 클래스에 정의된 종속 항목을 초기화하는 것입니다.
6. UserPreferencesRepository 사용
ViewModel에 저장소 제공
이제 종속 항목 삽입을 통해 UserPreferencesRepository를 사용할 수 있으므로 DessertReleaseViewModel에서 이를 사용할 수 있습니다.
- DessertReleaseViewModel에서- UserPreferencesRepository속성을 생성자 매개변수로 만듭니다.
class DessertReleaseViewModel(
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    ...
}
- ViewModel의 컴패니언 객체 내- viewModelFactory initializer블록에서 다음 코드를 사용하여- DessertReleaseApplication의 인스턴스를 가져옵니다.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                ...
            }
        }
    }
}
- DessertReleaseViewModel의 인스턴스를 만들고- userPreferencesRepository를 전달합니다.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                DessertReleaseViewModel(application.userPreferencesRepository)
            }
        }
    }
}
이제 ViewModel에서 UserPreferencesRepository에 액세스할 수 있습니다. 다음 단계에서는 이전에 구현한 UserPreferencesRepository의 읽기 및 쓰기 기능을 사용합니다.
레이아웃 환경설정 저장
- DessertReleaseViewModel에서- selectLayout()함수를 수정하여 환경설정 저장소에 액세스하고 레이아웃 환경설정을 업데이트합니다.
- DataStore에 쓰는 작업은- suspend함수를 사용하여 비동기식으로 실행됩니다. 새 코루틴을 시작하여 환경설정 저장소의- saveLayoutPreference()함수를 호출합니다.
fun selectLayout(isLinearLayout: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.saveLayoutPreference(isLinearLayout)
    }
}
레이아웃 환경설정 읽기
이 섹션에서는 ViewModel의 기존 uiState: StateFlow를 리팩터링하여 저장소의 isLinearLayout: Flow를 반영합니다.
- uiState속성을- MutableStateFlow(DessertReleaseUiState)로 초기화하는 코드를 삭제합니다.
val uiState: StateFlow<DessertReleaseUiState> =
저장소의 선형 레이아웃 환경설정에는 true 또는 false라는 가능한 두 값이 Flow<Boolean> 형식으로 있습니다. 이 값은 UI 상태에 매핑되어야 합니다.
- StateFlow를- isLinearLayout Flow에서 호출된- map()컬렉션 변환의 결과로 설정합니다.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
- DessertReleaseUiState데이터 클래스의 인스턴스를 반환하고- isLinearLayout Boolean을 전달합니다. 화면에서는 이 UI 상태를 사용하여 표시할 올바른 문자열과 아이콘을 결정합니다.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }
UserPreferencesRepository.isLinearLayout은 콜드 Flow입니다. 그러나 UI에 상태를 제공하려면 StateFlow와 같은 핫 흐름을 사용하는 것이 좋습니다. 그러면 UI에서 항상 상태를 즉시 사용할 수 있습니다.
- stateIn()함수를 사용하여- Flow를- StateFlow로 변환합니다.
- 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()
    )
- 앱을 실행합니다. 전환 아이콘을 클릭하면 그리드 레이아웃과 선형 레이아웃 간에 전환되는 것을 확인할 수 있습니다.
 
 
축하합니다. 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에서 확인하세요.
