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에서 확인하세요.