Preferences DataStore

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 스튜디오에서 열려면 다음을 실행합니다.

  1. Android 스튜디오를 시작합니다.
  2. Welcome to Android Studio 창에서 Get from VCS를 클릭합니다.

61c42d01719e5b6d.png

  1. Get from Version Control 대화상자에서 Version controlGit이 선택되어 있는지 확인합니다.

9284cfbe17219bbb.png

  1. 제공된 코드 URL을 URL 상자에 붙여넣습니다.
  2. 필요한 경우 Directory를 제안된 기본값과 다른 것으로 변경합니다.

5ddca7dd0d914255.png

  1. Clone을 클릭합니다. Android 스튜디오에서 코드를 가져오기 시작합니다.
  2. Android 스튜디오가 열릴 때까지 기다립니다.
  3. Codelab 시작 코드나 앱, 솔루션 코드에 적합한 모듈을 선택합니다.

2919fe3e0c79d762.png

  1. Run 버튼 8de56cba7583251f.png을 클릭하여 코드를 빌드하고 실행합니다.

2. 스타터 앱 개요

Words 앱은 두 화면으로 구성됩니다. 첫 번째 화면에는 사용자가 선택할 수 있는 문자가 표시되고 두 번째 화면에는 선택한 문자로 시작하는 단어 목록이 표시됩니다.

이 앱에는 사용자가 문자의 목록 레이아웃과 그리드 레이아웃 간에 전환할 수 있는 메뉴 옵션이 있습니다.

  1. 시작 코드를 다운로드하여 Android 스튜디오에서 열고 앱을 실행합니다. 문자가 선형 레이아웃으로 표시됩니다.
  2. 오른쪽 상단에 있는 메뉴 옵션을 탭합니다. 레이아웃이 그리드 레이아웃으로 전환됩니다.
  3. 앱을 종료하고 다시 실행합니다. Android 스튜디오에서 Stop 'app' f782441b99bdd0a4.pngRun 'app' d203bd07cbce5954.png 옵션을 사용하면 됩니다. 앱이 다시 실행되면 문자는 그리드가 아닌 선형 레이아웃으로 표시됩니다.

사용자가 선택한 설정이 유지되지 않습니다. 이 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를 종속 항목으로 추가하는 것입니다.

  1. 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 만들기

  1. data라는 패키지를 추가하고 그 안에 SettingsDataStore라는 Kotlin 클래스를 만듭니다.
  2. 생성자 매개변수를 Context 유형의 SettingsDataStore 클래스에 추가합니다.
class SettingsDataStore(context: Context) {}
  1. SettingsDataStore 클래스 외부에서 LAYOUT_PREFERENCES_NAME이라는 private const val을 선언하고 문자열 값 layout_preferences를 할당합니다. 다음 단계에서 인스턴스화할 Preferences DataStore의 이름입니다.
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
  1. 계속 클래스 외부에서 preferencesDataStore 위임을 사용하여 DataStore 인스턴스를 만듭니다. Preferences Datastore를 사용하고 있으므로 Preferences를 Datastore 유형으로 전달해야 합니다. 또한 Datastore nameLAYOUT_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 클래스에서 다음을 구현합니다.

  1. 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되도록 해야 합니다.

  1. saveLayoutToPreferencesStore()라는 suspend 함수를 만듭니다. 이 함수는 레이아웃 설정 불리언과 Context라는 두 매개변수를 사용합니다.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {

}
  1. 위의 함수를 구현하고 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 값을 가져옵니다.

  1. 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
   }
  1. 자동으로 가져오지 않는 경우 다음 가져오기를 추가합니다.
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

예외 처리

DataStore는 파일에서 데이터를 읽고 쓰므로 데이터에 액세스할 때 IOExceptions가 발생할 수 있습니다. catch() 연산자를 사용하여 예외를 포착해 이 문제를 처리합니다.

  1. 데이터를 읽는 동안 오류가 발생하면 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에서 다음 단계를 구현합니다.

  1. SettingsDataStore 유형의 SettingsDataStore라는 private 클래스 변수를 선언합니다. 이 변수를 lateinit로 설정합니다. 나중에 초기화하기 때문입니다.
private lateinit var SettingsDataStore: SettingsDataStore
  1. onViewCreated() 함수 끝에서 새 변수를 초기화하고 requireContext()SettingsDataStore 생성자에 전달합니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
}

데이터 읽기 및 관찰

  1. LetterListFragmentonViewCreated() 메서드 내 SettingsDataStore 초기화 아래에서 asLiveData()를 사용하여 preferenceFlowLivedata로 변환합니다. 관찰자를 연결하고 viewLifecycleOwner를 소유자로 전달합니다.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
  1. 관찰자 내에서 새 레이아웃 설정을 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 소유자가 소멸될 때 취소됩니다.

  1. LetterListFragmentonOptionsItemSelected() 함수 내 R.id.action_switch_layout 사례 끝에서 lifecycleScope를 사용하여 코루틴을 실행합니다. launch 블록 내에서 isLinearLayoutManagercontext를 전달하는 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
       }
  1. 앱을 실행합니다. 메뉴 옵션을 클릭하여 앱 레이아웃을 변경합니다.

  1. 이제 Preferences DataStore의 지속성을 테스트합니다. 앱 레이아웃을 그리드 레이아웃으로 변경합니다. 앱을 종료하고 다시 실행합니다. Android 스튜디오에서 Stop 'app' f782441b99bdd0a4.pngRun 'app' d203bd07cbce5954.png 옵션을 사용하면 됩니다.

cd2c31f27dfb5157.png

앱이 다시 실행되면 문자가 이제 선형 레이아웃이 아닌 그리드 레이아웃으로 표시됩니다. 앱이 사용자가 선택한 레이아웃 설정을 성공적으로 저장합니다.

문자는 이제 그리드 레이아웃으로 표시되지만 메뉴 아이콘은 올바르게 업데이트되지 않습니다. 이제 이 문제를 해결하는 방법을 살펴보겠습니다.

7. 메뉴 아이콘 버그 수정

메뉴 아이콘 버그가 발생하는 이유는 onViewCreated()에서 RecyclerView 레이아웃은 레이아웃 설정에 따라 업데이트되지만 메뉴 아이콘은 업데이트되지 않기 때문입니다. 이 문제는 RecyclerView 레이아웃을 업데이트하는 동시에 메뉴를 다시 그려 해결할 수 있습니다.

옵션 메뉴 다시 그리기

메뉴가 만들어지면 프레임마다 다시 그려지지 않습니다. 프레임마다 같은 메뉴를 다시 그리는 것이 중복되기 때문입니다. invalidateOptionsMenu() 함수는 Android에 옵션 메뉴를 다시 그리라고 지시합니다.

메뉴 항목 추가나 항목 삭제, 메뉴 텍스트나 아이콘 변경과 같이 옵션 메뉴에서 무언가를 변경할 때 이 함수를 호출할 수 있습니다. 이 경우에는 메뉴 아이콘이 변경되었습니다. 이 메서드 호출은 옵션 메뉴가 변경되었으므로 다시 만들어야 한다고 선언하는 것입니다. onCreateOptionsMenu(android.view.Menu) 메서드는 다음 번에 표시해야 할 때 호출됩니다.

  1. LetterListFragmentonViewCreated()preferenceFlow 관찰자 끝 chooseLayout() 호출 아래에서 실행합니다. activity에서 invalidateOptionsMenu()를 호출하여 메뉴를 다시 그립니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           ...
           // Redraw the menu
           activity?.invalidateOptionsMenu()
   })
}
  1. 앱을 다시 실행하고 레이아웃을 변경합니다.
  2. 앱을 종료하고 다시 실행합니다. 이제 메뉴 아이콘이 올바르게 업데이트되었습니다.

1c8cf63c8d175aad.png

마무리 앱에 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() 함수를 제공합니다.

10. 자세히 알아보기

블로그

기본적으로 Jetpack DataStore를 통해 데이터 저장하기