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