1. 시작하기 전에
이전 Codelab에서는 데이터베이스 추상화 레이어인 Room을 사용하여 SQLite 데이터베이스에 데이터를 저장하는 방법을 알아봤습니다. 이 Codelab에서는 Jetpack DataStore를 소개합니다. Kotlin 코루틴과 Flow에 기반하는 DataStore는 두 가지 구현을 제공합니다. 유형이 지정된 객체를 저장하는 Proto DataStore와 키-값 쌍을 저장하는 Preferences DataStore입니다.
이 실습 Codelab에서는 Preferences DataStore를 사용하는 방법을 알아봅니다. Proto DataStore는 이 Codelab에서 다루지 않습니다.
기본 요건
- Android 아키텍처 구성요소
ViewModel
,LiveData
,Flow
를 잘 알아야 하고ViewModelProvider.Factory
를 사용하여ViewModel
을 인스턴스화하는 방법을 알아야 합니다. - 동시 실행 기본사항을 잘 알아야 합니다.
- 장기 실행 작업에 코루틴을 사용하는 방법을 알아야 합니다.
학습할 내용
- DataStore의 정의와 사용 이유, 사용 시점
- 앱에 Preference DataStore를 추가하는 방법
필요한 항목
- Words 앱의 시작 코드(이전 Codelab의 Words 앱 솔루션 코드와 같음)
- Android 스튜디오가 설치된 컴퓨터
이 Codelab의 시작 코드 다운로드
이 Codelab에서는 이전 솔루션 코드에서 Words 앱의 기능을 확장합니다. 시작 코드에는 이전 Codelab을 통해 익숙한 코드도 포함될 수 있습니다.
GitHub에서 이 Codelab의 코드를 가져와 Android 스튜디오에서 열려면 다음을 실행합니다.
- Android 스튜디오를 시작합니다.
- Welcome to Android Studio 창에서 Get from VCS를 클릭합니다.
- Get from Version Control 대화상자에서 Version control에 Git이 선택되어 있는지 확인합니다.
- 제공된 코드 URL을 URL 상자에 붙여넣습니다.
- 필요한 경우 Directory를 제안된 기본값과 다른 것으로 변경합니다.
- Clone을 클릭합니다. Android 스튜디오에서 코드를 가져오기 시작합니다.
- Android 스튜디오가 열릴 때까지 기다립니다.
- Codelab 시작 코드나 앱, 솔루션 코드에 적합한 모듈을 선택합니다.
- Run 버튼 을 클릭하여 코드를 빌드하고 실행합니다.
2. 스타터 앱 개요
Words 앱은 두 화면으로 구성됩니다. 첫 번째 화면에는 사용자가 선택할 수 있는 문자가 표시되고 두 번째 화면에는 선택한 문자로 시작하는 단어 목록이 표시됩니다.
이 앱에는 사용자가 문자의 목록 레이아웃과 그리드 레이아웃 간에 전환할 수 있는 메뉴 옵션이 있습니다.
- 시작 코드를 다운로드하여 Android 스튜디오에서 열고 앱을 실행합니다. 문자가 선형 레이아웃으로 표시됩니다.
- 오른쪽 상단에 있는 메뉴 옵션을 탭합니다. 레이아웃이 그리드 레이아웃으로 전환됩니다.
- 앱을 종료하고 다시 실행합니다. Android 스튜디오에서 Stop 'app' 과 Run 'app' 옵션을 사용하면 됩니다. 앱이 다시 실행되면 문자는 그리드가 아닌 선형 레이아웃으로 표시됩니다.
사용자가 선택한 설정이 유지되지 않습니다. 이 Codelab에서는 이 문제를 해결하는 방법을 보여줍니다.
빌드할 항목
- 이 Codelab에서는 Preferences DataStore를 사용하여 DataStore에서 레이아웃 설정을 유지하는 방법을 알아봅니다.
3. Preferences DataStore 소개
Preferences DataStore는 로그인 세부정보 저장이나 어두운 모드 설정, 글꼴 크기 등 작고 간단한 데이터 세트에 적합합니다. DataStore는 온라인 식료품점 인벤토리 목록이나 학생 데이터베이스와 같은 복잡한 데이터 세트에는 적합하지 않습니다. 대용량 또는 복잡한 데이터 세트를 저장해야 한다면 DataStore 대신 Room을 사용하는 것이 좋습니다.
Jetpack DataStore 라이브러리를 사용하면 데이터 저장을 위한 간단하고 안전한 비동기 API를 만들 수 있습니다. Jetpack DataStore 라이브러리는 Preferences DataStore와 Proto DataStore라는 두 가지 구현을 제공합니다. Preferences DataStore와 Proto DataStore에서는 모두 데이터 저장이 가능하지만 저장 방법이 다릅니다.
- Preferences DataStore는 먼저 스키마(데이터베이스 모델)를 정의하지 않고 키에 기반하여 데이터에 액세스하고 저장합니다.
- Proto DataStore는 프로토콜 버퍼를 사용하여 스키마를 정의합니다. 프로토콜 버퍼(Protobuf)를 사용하면 강타입(strongly typed) 데이터를 유지할 수 있습니다. Protobuf는 XML 및 기타 유사한 데이터 형식보다 빠르고 작고 간결하며 덜 모호합니다.
Room과 Datastore 비교: 용도
애플리케이션에서 대용량/복잡한 데이터를 SQL과 같은 구조화된 형식으로 저장해야 한다면 Room을 사용하는 것이 좋습니다. 그러나 키-값 쌍으로 저장할 수 있는 간단하거나 적은 양의 데이터만 저장해야 한다면 DataStore가 적합합니다.
Proto DataStore와 Preferences DataStore 비교: 용도
Proto DataStore는 유형에 안전하고 효율적이지만 구성과 설정이 필요합니다. 앱 데이터가 키-값 쌍으로 저장할 수 있을 만큼 간단하다면 Preferences DataStore가 더 좋습니다. 훨씬 쉽게 설정할 수 있기 때문입니다.
Preferences DataStore를 종속 항목으로 추가
DataStore를 앱과 통합하는 첫 번째 단계는 DataStore를 종속 항목으로 추가하는 것입니다.
build.gradle(Module: Words.app)
에 다음 종속 항목을 추가합니다.
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
4. Preferences DataStore 만들기
data
라는 패키지를 추가하고 그 안에SettingsDataStore
라는 Kotlin 클래스를 만듭니다.- 생성자 매개변수를
Context
유형의SettingsDataStore
클래스에 추가합니다.
class SettingsDataStore(context: Context) {}
SettingsDataStore
클래스 외부에서LAYOUT_PREFERENCES_NAME
이라는private const val
을 선언하고 문자열 값layout_preferences
를 할당합니다. 다음 단계에서 인스턴스화할 Preferences DataStore의 이름입니다.
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
- 계속 클래스 외부에서
preferencesDataStore
위임을 사용하여DataStore
인스턴스를 만듭니다. Preferences Datastore를 사용하고 있으므로Preferences
를 Datastore 유형으로 전달해야 합니다. 또한 Datastorename
을LAYOUT_PREFERENCES_NAME
으로 설정합니다.
완성된 코드는 다음과 같습니다.
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
// Create a DataStore instance using the preferencesDataStore delegate, with the Context as
// receiver.
private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCES_NAME
)
5. SettingsDataStore 클래스 구현
앞에서 설명한 것처럼 Preferences DataStore는 키-값 쌍으로 데이터를 저장합니다. 이 단계에서는 레이아웃 설정을 저장하는 데 필요한 키를 정의하고 Preferences DataStore에 쓰고 Preferences DataStore에서 읽는 함수를 정의합니다.
키 유형 함수
Preferences DataStore는 Room과 같이 사전 정의된 스키마를 사용하지 않습니다. DataStore<Preferences>
인스턴스에 저장하는 각 값의 키를 정의하는 데는 상응하는 키 유형 함수를 사용합니다. 예를 들어 int
값의 키를 정의하려면 intPreferencesKey()
를 사용하고 string
값의 경우에는 stringPreferencesKey()
를 사용합니다. 일반적으로 이러한 함수 이름에는 키를 대상으로 저장하려는 데이터 유형이 접두사로 붙습니다.
data\SettingsDataStore
클래스에서 다음을 구현합니다.
SettingsDataStore
클래스를 구현하려면 먼저 사용자 설정이 선형 레이아웃인지 지정하는 불리언 값을 저장하는 키를 만듭니다.IS_LINEAR_LAYOUT_MANAGER
라는private
클래스 속성을 만들고is_linear_layout_manager
키 이름을 함수 매개변수로 전달하는booleanPreferencesKey()
를 사용하여 초기화합니다.
private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")
Preferences DataStore에 쓰기
이제 키를 사용하고 불리언 레이아웃 설정을 DataStore
에 저장해 보겠습니다. Preferences DataStore는 DataStore
의 데이터를 트랜잭션 방식으로 업데이트하는 edit()
정지 함수를 제공합니다. 함수의 변환 매개변수는 필요에 따라 값을 업데이트할 수 있는 코드 블록을 허용합니다. 변환 블록의 모든 코드는 단일 트랜잭션으로 취급됩니다. 내부적으로 트랜잭션 작업은 Dispacter.IO
로 이동하므로 edit()
함수를 호출할 때 함수가 suspend
되도록 해야 합니다.
saveLayoutToPreferencesStore()
라는suspend
함수를 만듭니다. 이 함수는 레이아웃 설정 불리언과Context
라는 두 매개변수를 사용합니다.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
}
- 위의 함수를 구현하고
dataStore
.edit()
을 호출한 후 코드 블록을 전달하여 새 값을 저장합니다.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
context.dataStore.edit { preferences ->
preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager
}
}
Preferences DataStore에서 읽기
Preferences DataStore는 환경설정이 변경될 때마다 내보내는 Flow<Preferences>
에 저장된 데이터를 노출합니다. 전체 Preferences
객체를 노출하지 않고 Boolean
값만 노출하는 것이 좋습니다. 이를 위해 Flow<Preferences>
를 매핑하고 관심 있는 Boolean
값을 가져옵니다.
dataStore.data: Flow<Preferences>
에 기반하여 구성된preferenceFlow: Flow<UserPreferences>
를 노출하고Boolean
환경설정을 가져오도록 매핑합니다. 처음 실행할 때 Datastore가 비어 있으므로 기본적으로true
가 반환됩니다.
val preferenceFlow: Flow<Boolean> = context.dataStore.data
.map { preferences ->
// On the first run of the app, we will use LinearLayoutManager by default
preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
}
- 자동으로 가져오지 않는 경우 다음 가져오기를 추가합니다.
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
예외 처리
DataStore는 파일에서 데이터를 읽고 쓰므로 데이터에 액세스할 때 IOExceptions
가 발생할 수 있습니다. catch()
연산자를 사용하여 예외를 포착해 이 문제를 처리합니다.
- 데이터를 읽는 동안 오류가 발생하면 SharedPreference DataStore에서
IOException
이 발생합니다.preferenceFlow
선언에서map()
앞에catch()
연산자를 사용하여IOException
을 포착하고emptyPreferences()
를 내보냅니다. 여기서는 다른 유형의 예외를 예상하지 않으므로 편의를 위해 다른 유형의 예외가 발생한다면 예외를 다시 발생시킵니다.
val preferenceFlow: Flow<Boolean> = context.dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences ->
// On the first run of the app, we will use LinearLayoutManager by default
preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
}
이제 data\SettingsDataStore
클래스를 사용할 수 있습니다.
6. SettingsDataStore 클래스 사용
다음 작업에서는 LetterListFragment
클래스에서 SettingsDataStore
를 사용합니다. 레이아웃 설정에 관찰자를 연결하고 적절하게 UI를 업데이트합니다.
LetterListFragment
에서 다음 단계를 구현합니다.
SettingsDataStore
유형의SettingsDataStore
라는private
클래스 변수를 선언합니다. 이 변수를lateinit
로 설정합니다. 나중에 초기화하기 때문입니다.
private lateinit var SettingsDataStore: SettingsDataStore
onViewCreated()
함수 끝에서 새 변수를 초기화하고requireContext()
를SettingsDataStore
생성자에 전달합니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
// Initialize SettingsDataStore
SettingsDataStore = SettingsDataStore(requireContext())
}
데이터 읽기 및 관찰
LetterListFragment
의onViewCreated()
메서드 내SettingsDataStore
초기화 아래에서asLiveData
()
를 사용하여preferenceFlow
를Livedata
로 변환합니다. 관찰자를 연결하고viewLifecycleOwner
를 소유자로 전달합니다.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
- 관찰자 내에서 새 레이아웃 설정을
isLinearLayoutManager
변수에 할당합니다.chooseLayout()
함수를 호출하여 RecyclerView 레이아웃을 업데이트합니다.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
isLinearLayoutManager = value
chooseLayout()
})
완성된 onViewCreated()
함수는 다음과 같습니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerView = binding.recyclerView
// Initialize SettingsDataStore
SettingsDataStore = SettingsDataStore(requireContext())
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
isLinearLayoutManager = value
chooseLayout()
})
}
DataStore에 레이아웃 설정 쓰기
마지막 단계는 사용자가 메뉴 옵션을 탭할 때 Preferences DataStore에 레이아웃 설정을 쓰는 것입니다. Preference Datastore에 데이터 쓰기는 코루틴 내에서 비동기적으로 실행되어야 합니다. 프래그먼트 내에서 이 작업을 실행하려면 LifecycleScope
라는 CoroutineScope
를 사용하세요.
LifecycleScope
프래그먼트와 같은 수명 주기 인식 구성요소는 LiveData
와의 상호 운용성 레이어와 함께 앱의 논리적 범위에 관한 최고 수준의 코루틴 지원을 제공합니다. LifecycleScope
는 각 Lifecycle
객체에서 정의됩니다. 이 범위에서 실행된 코루틴은 Lifecycle
소유자가 소멸될 때 취소됩니다.
LetterListFragment
의onOptionsItemSelected()
함수 내R.id.
action_switch_layout
사례 끝에서lifecycleScope
를 사용하여 코루틴을 실행합니다.launch
블록 내에서isLinearLayoutManager
와context
를 전달하는saveLayoutToPreferencesStore()
를 호출합니다.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_switch_layout -> {
...
// Launch a coroutine and write the layout setting in the preference Datastore
lifecycleScope.launch {
SettingsDataStore.saveLayoutToPreferencesStore(isLinearLayoutManager, requireContext())
}
...
return true
}
- 앱을 실행합니다. 메뉴 옵션을 클릭하여 앱 레이아웃을 변경합니다.
- 이제 Preferences DataStore의 지속성을 테스트합니다. 앱 레이아웃을 그리드 레이아웃으로 변경합니다. 앱을 종료하고 다시 실행합니다. Android 스튜디오에서 Stop 'app' 과 Run 'app' 옵션을 사용하면 됩니다.
앱이 다시 실행되면 문자가 이제 선형 레이아웃이 아닌 그리드 레이아웃으로 표시됩니다. 앱이 사용자가 선택한 레이아웃 설정을 성공적으로 저장합니다.
문자는 이제 그리드 레이아웃으로 표시되지만 메뉴 아이콘은 올바르게 업데이트되지 않습니다. 이제 이 문제를 해결하는 방법을 살펴보겠습니다.
7. 메뉴 아이콘 버그 수정
메뉴 아이콘 버그가 발생하는 이유는 onViewCreated()
에서 RecyclerView 레이아웃은 레이아웃 설정에 따라 업데이트되지만 메뉴 아이콘은 업데이트되지 않기 때문입니다. 이 문제는 RecyclerView 레이아웃을 업데이트하는 동시에 메뉴를 다시 그려 해결할 수 있습니다.
옵션 메뉴 다시 그리기
메뉴가 만들어지면 프레임마다 다시 그려지지 않습니다. 프레임마다 같은 메뉴를 다시 그리는 것이 중복되기 때문입니다. invalidateOptionsMenu()
함수는 Android에 옵션 메뉴를 다시 그리라고 지시합니다.
메뉴 항목 추가나 항목 삭제, 메뉴 텍스트나 아이콘 변경과 같이 옵션 메뉴에서 무언가를 변경할 때 이 함수를 호출할 수 있습니다. 이 경우에는 메뉴 아이콘이 변경되었습니다. 이 메서드 호출은 옵션 메뉴가 변경되었으므로 다시 만들어야 한다고 선언하는 것입니다. onCreateOptionsMenu(android.view.Menu)
메서드는 다음 번에 표시해야 할 때 호출됩니다.
LetterListFragment
의onViewCreated()
내preferenceFlow
관찰자 끝chooseLayout()
호출 아래에서 실행합니다.activity
에서invalidateOptionsMenu()
를 호출하여 메뉴를 다시 그립니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
...
// Redraw the menu
activity?.invalidateOptionsMenu()
})
}
- 앱을 다시 실행하고 레이아웃을 변경합니다.
- 앱을 종료하고 다시 실행합니다. 이제 메뉴 아이콘이 올바르게 업데이트되었습니다.
마무리 앱에 Preferences DataStore를 추가하여 사용자 선택을 저장했습니다.
8. 솔루션 코드
이 Codelab의 솔루션 코드는 아래에 나온 프로젝트와 모듈에 있습니다.
9. 요약
- DataStore에는 Kotlin 코루틴과 Flow를 사용하는 완전 비동기 API가 있으므로 데이터 일관성이 보장됩니다.
- Jetpack DataStore는 프로토콜 버퍼를 사용하여 키-값 쌍 또는 유형이 지정된 객체를 저장할 수 있는 데이터 저장소 솔루션입니다.
- DataStore는 Preferences DataStore와 Proto DataStore라는 두 가지 구현을 제공합니다.
- Preferences DataStore는 사전 정의된 스키마를 사용하지 않습니다.
- Preferences DataStore는 상응하는 키 유형 함수를 사용하여
DataStore<Preferences>
인스턴스에 저장해야 하는 각 값의 키를 정의합니다. 예를 들어int
값의 키를 정의하려면intPreferencesKey()
를 사용합니다. - Preferences DataStore는
DataStore
의 데이터를 트랜잭션 방식으로 업데이트하는edit()
함수를 제공합니다.