Proto Datastore를 사용하여 작업하기

1. 소개

Datastore란?

Datastore는 개선된 신규 데이터 저장소 솔루션으로, SharedPreferences를 대체합니다. Kotlin 코루틴과 Flow를 기반으로 한 Datastore는 서로 다른 두 가지 구현, 즉 타입 객체를 저장하는 Proto Datastore(프로토콜 버퍼로 지원됨) 및 키-값 쌍을 저장하는 Preferences Datastore를 제공합니다. 비동기적이고 일관된 트랜잭션 방식으로 데이터를 저장하여 SharedPreferences의 일부 단점을 해결합니다.

학습할 내용

  • Datastore 특성 및 Datastore를 사용해야 하는 이유
  • 프로젝트에 Datastore를 추가하는 방법
  • Preferences Datastore와 Proto Datastore의 차이점 및 각각의 장점
  • Proto Datastore를 사용하는 방법
  • SharedPreferences에서 Proto Datastore로 이전하는 방법

빌드할 항목

이 Codelab에서는 먼저 작업 목록을 표시하는 샘플 앱을 사용합니다. 작업 목록은 완료 상태를 기준으로 필터링하고 우선순위와 기한을 기준으로 정렬할 수 있습니다.

fcb2ffa4e6b77f33.gif

Show completed tasks 필터의 부울 플래그가 메모리에 저장됩니다. 정렬 순서는 SharedPreferences 객체를 사용하여 디스크에 유지됩니다.

Datastore에는 Preferences Datastore와 Proto Datastore라는 서로 다른 두 가지 구현이 있기 때문에 각 구현에서 다음 작업을 완료하여 Proto Datastore를 사용하는 방법을 알아봅니다.

  • Datastore에 완료 상태 필터를 유지합니다.
  • 정렬 순서를 SharedPreferences에서 Datastore로 이전합니다.

둘 사이의 차이를 더 잘 이해할 수 있도록 Preferences Datastore Codelab도 살펴보는 것이 좋습니다.

필요한 항목

아키텍처 구성요소에 관한 소개는 뷰 Codelab이 있는 Room을 확인하세요. Flow에 관한 소개는 Kotlin Flow 및 LiveData Codelab을 사용한 고급 코루틴을 확인하세요.

2. 설정

이 단계에서는 전체 Codelab을 위한 코드를 다운로드한 후 간단한 예시 앱을 실행합니다.

Google에서 준비한 시작 프로젝트를 사용하면 신속하게 빌드할 수 있습니다.

git을 설치했다면 아래 명령어를 실행하면 됩니다. git이 설치되어 있는지 확인하려면 터미널이나 명령줄에 git --version을 입력하여 정확하게 실행되는지 확인합니다.

 git clone https://github.com/googlecodelabs/android-datastore

초기 상태는 master 분기에 있으며, 솔루션 코드는 proto_datastore 분기에 있습니다.

git이 없는 경우 다음 버튼을 클릭하여 이 Codelab을 위한 모든 코드를 다운로드하세요.

소스 코드 다운로드

  1. 코드의 압축을 푼 다음 Android 스튜디오 Arctic Fox에서 프로젝트를 엽니다.
  2. 기기 또는 에뮬레이터에서 app 실행 구성을 실행합니다.

b3c0dfdb92dfed77.png

앱이 실행되고 다음 작업 목록이 표시됩니다.

d3972939a2de88ba.png

3. 프로젝트 개요

앱에서 작업 목록을 볼 수 있습니다. 각 작업에는 이름, 완료 상태, 우선순위, 기한과 같은 속성이 있습니다.

처리해야 하는 코드를 단순화하기 위해 개발자는 앱에서 다음 두 가지 작업만 할 수 있습니다.

  • 완료된 작업 표시 여부 전환(기본적으로 완료된 작업은 숨겨짐)
  • 우선순위, 기한 또는 기한과 우선순위를 기준으로 작업 정렬

앱은 앱 아키텍처 가이드에서 권장하는 아키텍처를 따릅니다. 각 패키지에는 다음과 같은 항목이 포함됩니다.

data

  • Task 모델 클래스
  • TasksRepository 클래스: 작업을 제공합니다. 간단하게 이 클래스는 하드코딩된 데이터를 반환하고 Flow를 통해 노출하여 더욱 현실적인 시나리오를 표현합니다.
  • UserPreferencesRepository 클래스: enum으로 정의된 SortOrder를 포함합니다. 현재 정렬 순서가 SharedPreferences에 저장되며 enum 값 이름에 따라 String으로 저장됩니다. 이 클래스는 정렬 순서를 저장하고 가져오는 동기 메서드를 노출합니다.

ui

  • RecyclerView가 있는 Activity 표시와 관련된 클래스입니다.
  • TasksViewModel 클래스는 UI 로직을 담당합니다.

TasksViewModel: UI에 표시해야 하는 데이터를 빌드하는 데 필요한 모든 요소를 포함합니다. 이러한 요소에는 작업 목록, showCompletedsortOrder 플래그가 있으며 TasksUiModel 객체에 래핑되어 있습니다. 이러한 값 중 하나가 변경될 때마다 TasksUiModel을 새로 재구성해야 합니다. 이를 위해 다음 세 가지 요소를 결합합니다.

  • Flow<List<Task>>: TasksRepository에서 가져옵니다.
  • MutableStateFlow<Boolean>: 메모리에만 유지되고 있는 최신 showCompleted 플래그를 포함합니다.
  • MutableStateFlow<SortOrder>: 최신 sortOrder 값을 포함합니다.

활동이 시작될 때만 UI를 올바르게 업데이트할 수 있도록 LiveData<TasksUiModel>을 노출합니다.

코드에 다음과 같은 문제가 있습니다.

  • UserPreferencesRepository.sortOrder를 초기화할 때 디스크 IO에서 UI 스레드를 차단합니다. 이로 인해 UI 버벅거림이 발생할 수 있습니다.
  • showCompleted 플래그는 메모리에만 보관되므로 사용자가 앱을 열 때마다 재설정됩니다. SortOrder와 마찬가지로 앱을 닫은 후에도 플래그가 유지되어야 합니다.
  • 현재 SharedPreferences를 사용하여 데이터를 보존하지만, 직접 수정할 수 있는 MutableStateFlow를 메모리에 보관하여 변경 시 알림을 받을 수 있습니다. 값이 애플리케이션의 다른 위치에서 수정되는 경우 알림을 받지 못합니다.
  • UserPreferencesRepository에서는 정렬 순서를 업데이트하는 두 가지 메서드, 즉 enableSortByDeadline()enableSortByPriority()를 노출합니다. 두 메서드는 현재 정렬 순서 값에 의존하지만, 한 메서드가 완료되기 전에 다른 한 메서드가 호출되면 최종값이 잘못 생성됩니다. 또한 이러한 메서드는 UI 스레드에서 호출될 때 UI 버벅거림 및 엄격 모드 위반을 일으킬 수 있습니다.

showCompletedsortOrder 플래그는 모두 사용자 환경설정이지만, 현재 두 개의 다른 객체로 표현됩니다. 이러한 이유로 UserPreferences 클래스에 두 플래그를 통합하는 것이 앞으로 실행할 목표 중 하나입니다.

Datastore를 사용하여 이러한 문제를 해결하는 방법을 알아보겠습니다.

4. Datastore - 기본사항

소규모 또는 단순한 데이터 세트를 저장해야 하는 경우가 있을 수 있습니다. 이를 위해 이전에는 SharedPreferences를 사용했지만, 이 API에는 일련의 단점이 있습니다. Jetpack Datastore 라이브러리는 이러한 문제를 해결하고 데이터 저장을 위한 간단하고 더 안전한 비동기 API를 만드는 것을 목표로 합니다. 이 라이브러리에서는 다음의 서로 다른 두 가지 구현을 제공합니다.

  • Preferences Datastore
  • Proto Datastore

기능

SharedPreferences

PreferencesDatastore

ProtoDatastore

비동기 API

✅(변경된 값을 읽는 용도로만, 리스너를 통해)

✅(Flow와 RxJava 2 & 3 Flowable을 통해)

✅(Flow와 RxJava 2 & 3 Flowable을 통해)

동기 API

✅(단, UI 스레드에서 호출하는 것은 안전하지 않음)

UI 스레드에서 호출하기에 안전함

❌(1)

✅(작업은 내부에서 Dispatchers.IO로 이동됨)

✅(작업은 내부에서 Dispatchers.IO로 이동됨)

오류 신호 전송 가능

런타임 예외로부터 안전함

❌(2)

strong consistency가 보장되는 트랜잭션 API가 있음

데이터 이전 처리

유형 안전성

✅(프로토콜 버퍼 포함)

(1) SharedPreferences에는 UI 스레드에서 호출하기에 안전해 보일 수 있지만 실제로는 디스크 I/O 작업을 하는 동기 API가 있습니다. 또한 apply()fsync()에서 UI 스레드를 차단합니다. 대기 중인 fsync() 호출은 서비스가 시작되거나 중지될 때마다, 그리고 애플리케이션에서 활동이 시작되거나 중지될 때마다 트리거됩니다. UI 스레드는 apply()에서 예약한 대기 중인 fsync() 호출에서 차단되며 주로 ANR의 소스가 됩니다.

(2) SharedPreferences는 파싱 오류를 런타임 예외로 발생시킵니다.

Preferences Datastore와 Proto Datastore 비교

Preferences Datastore와 Proto Datastore에서는 모두 데이터 저장이 가능하지만 저장 방법이 서로 다릅니다.

  • Preference Datastore는 SharedPreferences와 마찬가지로 스키마를 먼저 정의하지 않은 상태에서 키를 기반으로 데이터에 액세스합니다.
  • Proto Datastore프로토콜 버퍼를 사용하여 스키마를 정의합니다. Protobuf를 사용하기 때문에 강타입(strongly typed) 데이터를 유지할 수 있습니다. 이러한 데이터는 XML 등 다른 유사한 데이터 형식보다 빠르고 작고 간결하며 덜 모호합니다. Proto Datastore를 사용하려면 새로운 직렬화 메커니즘을 배워야 하지만 Proto Datastore의 강타입 이점이 그만한 가치가 있습니다.

Room과 Datastore 비교

부분 업데이트, 참조 무결성 또는 대규모/복잡한 데이터 세트가 필요한 경우에는 Datastore 대신 Room을 사용하는 것이 좋습니다. Datastore는 소규모 또는 단순한 데이터 세트에 적합하며 부분 업데이트나 참조 무결성을 지원하지 않습니다.

5. Proto Datastore - 개요

SharedPreferences 및 Preferences Datastore의 단점 중 하나는 스키마를 정의하거나 올바른 유형으로 키에 액세스할 방법이 없다는 점입니다. Proto Datastore는 프로토콜 버퍼를 사용해 스키마를 정의하는 방식으로 이 문제를 해결합니다. Proto Datastore를 사용하면 저장된 유형을 인식하고 제공하기 때문에 키를 사용할 필요가 없게 됩니다.

Proto Datastore와 Protobuf를 프로젝트에 추가하는 방법, 프로토콜 버퍼의 특성, 프로토콜 버퍼를 Proto DataStore에서 사용하는 방법, SharedPreferences를 Datastore로 이전하는 방법을 살펴보겠습니다.

종속 항목 추가

Proto Datastore를 사용하고 스키마용 코드 생성을 위해 Protobuf를 가져오려면 다음과 같은 작업으로 build.gradle 파일을 조금 변경해야 합니다.

  • Protobuf 플러그인 추가
  • Protobuf 및 Proto Datastore 종속 항목 추가
  • Protobuf 구성
plugins {
    ...
    id "com.google.protobuf" version "0.8.17"
}

dependencies {
    implementation  "androidx.datastore:datastore-core:1.0.0"
    implementation  "com.google.protobuf:protobuf-javalite:3.18.0"
    ...
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.14.0"
    }

    // Generates the java Protobuf-lite code for the Protobufs in this project. See
    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
    // for more information.
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}

6. protobuf 객체 정의 및 사용

프로토콜 버퍼는 구조화된 데이터를 직렬화하는 메커니즘입니다. 개발자가 데이터를 어떻게 구조화할지 한 번 정의하면 컴파일러가 구조화된 데이터를 쉽게 쓰고 읽을 수 있도록 소스 코드를 생성합니다.

proto 파일 만들기

proto 파일에서 스키마를 정의합니다. 이 Codelab에는 show_completedsort_order, 두 개의 사용자 환경설정이 있으며 현재 두 개의 다른 객체로 표현됩니다. 이러한 이유로 Datastore에 저장된 UserPreferences 클래스 아래에 이러한 두 플래그를 통합하고자 합니다. 이 클래스를 Kotlin에서 정의하는 대신, protobuf 스키마에서 정의할 것입니다.

구문에 관한 세부정보는 Proto 언어 가이드를 확인하세요. 이 Codelab에서는 필요한 유형에만 집중하겠습니다.

app/src/main/proto 디렉터리에 user_prefs.proto라는 새 파일을 만듭니다. 이 폴더 구조가 표시되지 않으면 프로젝트 뷰로 전환합니다. protobuf에서 각 구조는 message 키워드를 사용하여 정의됩니다. 구조의 각 요소는 유형과 이름에 따라 메시지 내에 정의되며 1부터 차례로 순서가 할당됩니다. 우선은 show_completed라는 부울 값이 있는 UserPreferences 메시지를 정의해 보겠습니다.

syntax = "proto3";

option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;

message UserPreferences {
  // filter for showing / hiding completed tasks
  bool show_completed = 1;
}

serializer 만들기

proto 파일에 정의한 데이터 유형을 읽고 쓰는 방법을 Datastore에 알리려면 serializer를 구현해야 합니다. 또한 serializer는 디스크에 데이터가 없는 경우 반환될 기본값을 정의합니다. data 패키지에 UserPreferencesSerializer라는 새 파일을 만듭니다.

object UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
    override suspend fun readFrom(input: InputStream): UserPreferences {
        try {
            return UserPreferences.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}

7. Proto Datastore에서 데이터 유지

Datastore 만들기

showCompleted 플래그는 메모리의 TasksViewModel에 유지되지만, Datastore 인스턴스의 UserPreferencesRepository에 저장되어야 합니다.

Datastore 인스턴스를 만들기 위해 dataStore 위임을 사용하며 수신기로 Context를 사용합니다. 이 위임에는 다음과 같은 두 가지 필수 매개변수가 있습니다.

  • Datastore가 작동할 파일의 이름
  • Datastore에 사용되는 유형을 위한 serializer. (이 경우에는 UserPreferencesSerializer임).

이 Codelab에서는 간단하게 이를 TasksActivity에서 처리합니다.

private const val USER_PREFERENCES_NAME = "user_preferences"
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"
private const val SORT_ORDER_KEY = "sort_order"

private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = UserPreferencesSerializer
)

dataStore 위임은 애플리케이션에 이 이름을 가진 Datastore 인스턴스가 하나만 있음을 보장합니다. 현재, UserPreferencesRepositorysortOrderFlow를 포함하고 TasksActivity의 수명 주기에 직접 연결되는 것을 회피하기 위해 싱글톤으로 구현되었습니다. UserPreferenceRepository는 Datastore의 데이터를 사용할 뿐 새로운 객체를 만들거나 포함하지 않으므로 다음과 같이 싱글톤 구현을 삭제할 수 있습니다.

  • companion object 삭제
  • constructor를 공개로 변경

UserPreferencesRepository는 생성자 매개변수로 DataStore 인스턴스를 가져와야 합니다. 현재는 SharedPreferences에서 Context를 필요로 하므로 매개변수로 남겨둘 수 있지만, 향후 삭제할 예정입니다.

class UserPreferencesRepository(
    private val userPreferencesStore: DataStore<UserPreferences>,
    context: Context
) { ... }

이제 TasksActivity에서 UserPreferencesRepository의 생성자를 업데이트하고 dataStore에 전달합니다.

viewModel = ViewModelProvider(
    this,
    TasksViewModelFactory(
        TasksRepository,
        UserPreferencesRepository(dataStore, this)
    )
).get(TasksViewModel::class.java)

Proto Datastore에서 데이터 읽기

Proto Datastore는 Flow<UserPreferences>에 저장된 데이터를 노출합니다. dataStore.data가 할당된 공개 userPreferencesFlow: Flow<UserPreferences> 값을 만들어 보겠습니다.

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data

데이터를 읽는 동안 예외 처리

Datastore가 파일에서 데이터를 읽을 때 오류가 발생하면 IOException이 발생합니다. 이 문제는 catch Flow 변환을 사용하여 처리할 수 있으며, 오류를 기록해 놓으면 됩니다.

private val TAG: String = "UserPreferencesRepo"

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
        // dataStore.data throws an IOException when an error is encountered when reading data
        if (exception is IOException) {
            Log.e(TAG, "Error reading sort order preferences.", exception)
            emit(UserPreferences.getDefaultInstance())
        } else {
            throw exception
        }
    }

Proto Datastore에 데이터 쓰기

데이터를 쓸 수 있도록 Datastore는 정지 DataStore.updateData() 함수를 제공합니다. 이 함수에서 UserPreferences의 현재 상태를 매개변수로 가져올 수 있습니다. 이를 업데이트하려면 preferences 객체를 빌더로 변환하고 새 값을 설정한 다음 새 preferences를 빌드해야 합니다.

updateData()는 원자적 읽기-쓰기-수정 작업을 통해 트랜잭션 방식으로 데이터를 업데이트합니다. 코루틴은 데이터가 디스크에 유지되면 완료됩니다.

UserPreferencesshowCompleted 속성을 업데이트할 수 있는 updateShowCompleted()라는 정지 함수를 만들겠습니다. 이 함수는 dataStore.updateData()를 호출하고 새 값을 설정합니다.

suspend fun updateShowCompleted(completed: Boolean) {
    dataStore.updateData { preferences ->
        preferences.toBuilder().setShowCompleted(completed).build()
    }
}

이 시점에서 앱을 컴파일해야 하지만 UserPreferencesRepository에서 방금 만든 기능은 사용되지 않습니다.

8. SharedPreferences에서 Proto Datastore로 옮기기

proto에 저장할 데이터 정의

정렬 순서는 SharedPreferences에 저장됩니다. 정렬 순서를 Datastore로 옮기겠습니다. 이렇게 하려면 정렬 순서도 저장할 수 있도록 먼저 proto 파일의 UserPreferences를 업데이트합니다. SortOrderenum이므로 UserPreference에 정의해야 합니다. enums는 Kotlin과 마찬가지로 protobuf에 정의됩니다.

열거형의 경우 기본값은 enum의 유형 정의에 나열된 첫 번째 값입니다. 그러나 SharedPreferences에서 이전할 경우 가져온 값이 기본값인지 아니면 이전에 SharedPreferences에 설정된 값인지 알아야 합니다. 이를 위해 SortOrder enum에 새 값(UNSPECIFIED)을 정의하고 제일 먼저 나열합니다. 그러면 정의한 값이 기본값이 될 수 있습니다.

user_prefs.proto 파일은 다음과 같이 표시됩니다.

syntax = "proto3";

option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;

message UserPreferences {
  // filter for showing / hiding completed tasks
  bool show_completed = 1;

  // defines tasks sorting order: no order, by deadline, by priority, by deadline and priority
  enum SortOrder {
    UNSPECIFIED = 0;
    NONE = 1;
    BY_DEADLINE = 2;
    BY_PRIORITY = 3;
    BY_DEADLINE_AND_PRIORITY = 4;
  }

  // user selected tasks sorting order
  SortOrder sort_order = 2;
}

UserPreferences 객체가 생성되도록 프로젝트를 지우고 다시 빌드합니다. 이때 새 필드를 포함합니다.

proto 파일에 SortOrder가 정의되었으므로 UserPreferencesRepository에서 선언을 삭제해도 됩니다. 다음을 삭제합니다.

enum class SortOrder {
    NONE,
    BY_DEADLINE,
    BY_PRIORITY,
    BY_DEADLINE_AND_PRIORITY
}

모든 곳에서 올바른 SortOrder 가져오기를 사용해야 합니다.

import com.codelab.android.datastore.UserPreferences.SortOrder

TasksViewModel.filterSortTasks()에서는 SortOrder 유형에 따라 다른 작업을 합니다. UNSPECIFIED 옵션도 추가했으므로 when(sortOrder) 문에 또 다른 경우를 추가해야 합니다. 현재 옵션 외에 다른 옵션은 처리하고 싶지 않기 때문에 다른 경우에는 UnsupportedOperationException을 발생시키면 됩니다.

이제 filterSortTasks() 함수는 다음과 같습니다.

private fun filterSortTasks(
    tasks: List<Task>,
    showCompleted: Boolean,
    sortOrder: SortOrder
): List<Task> {
    // filter the tasks
    val filteredTasks = if (showCompleted) {
        tasks
    } else {
        tasks.filter { !it.completed }
    }
    // sort the tasks
    return when (sortOrder) {
        SortOrder.UNSPECIFIED -> filteredTasks
        SortOrder.NONE -> filteredTasks
        SortOrder.BY_DEADLINE -> filteredTasks.sortedByDescending { it.deadline }
        SortOrder.BY_PRIORITY -> filteredTasks.sortedBy { it.priority }
        SortOrder.BY_DEADLINE_AND_PRIORITY -> filteredTasks.sortedWith(
            compareByDescending<Task> { it.deadline }.thenBy { it.priority }
        )
        // We shouldn't get any other values
        else -> throw UnsupportedOperationException("$sortOrder not supported")
    }
}

SharedPreferences에서 이전

이전 작업을 지원하기 위해 Datastore는 SharedPreferencesMigration 클래스를 정의합니다. TasksActivity에서 사용할 Datastore를 만드는 by dataStore 메서드는 produceMigrations 매개변수도 노출합니다. 이 블록에서 이 Datastore 인스턴스를 위해 실행될 DataMigration의 목록을 만듭니다. 여기서는 하나의 이전(SharedPreferencesMigration)만 사용합니다.

migrate 블록은 SharedPreferencesMigration 구현 시 두 개의 매개변수를 제공합니다.

  • SharedPreferencesView: SharedPreferences에서 데이터를 가져올 수 있도록 허용합니다.
  • UserPreferences: 현재 데이터입니다.

UserPreferences 객체를 반환해야 합니다.

migrate 블록을 구현할 때 다음 단계를 따라야 합니다.

  1. UserPreferences에서 sortOrder 값을 확인합니다.
  2. SortOrder.UNSPECIFIED이면 SharedPreferences에서 값을 가져와야 한다는 의미입니다. SortOrder가 누락되어 있으면 SortOrder.NONE을 기본값으로 사용할 수 있습니다.
  3. 정렬 순서를 가져온 후에는 UserPreferences 객체를 빌더로 변환하고 정렬 순서를 설정한 다음 build()를 호출하여 객체를 다시 빌드해야 합니다. 이 변경사항은 다른 필드에 아무런 영향을 미치지 않습니다.
  4. UserPreferencessortOrder 값이 SortOrder.UNSPECIFIED가 아닌 경우 이미 이전이 성공적으로 완료되었을 것이므로 migrate에서 가져온 현재 데이터를 반환하면 됩니다.
private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = UserPreferencesSerializer,
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context,
                USER_PREFERENCES_NAME
            ) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
                // Define the mapping from SharedPreferences to UserPreferences
                if (currentData.sortOrder == SortOrder.UNSPECIFIED) {
                    currentData.toBuilder().setSortOrder(
                        SortOrder.valueOf(
                            sharedPrefs.getString(SORT_ORDER_KEY, SortOrder.NONE.name)!!
                        )
                    ).build()
                } else {
                    currentData
                }
            }
        )
    }
)

이전 로직을 정의했으므로 정의한 이전 로직을 사용해야 한다고 DataStore에 알려야 합니다. 이를 위해 Datastore 빌더를 업데이트하고 migrations 매개변수에 SharedPreferencesMigration의 인스턴스를 포함하는 새 목록을 할당합니다.

private val dataStore: DataStore<UserPreferences> = context.createDataStore(
    fileName = "user_prefs.pb",
    serializer = UserPreferencesSerializer,
    migrations = listOf(sharedPrefsMigration)
)

정렬 순서를 Datastore에 저장

enableSortByDeadline()enableSortByPriority()가 호출될 때 정렬 순서를 업데이트하려면 다음을 해야 합니다.

  • dataStore.updateData()의 람다에서 각 기능을 호출합니다.
  • updateData()가 정지 함수이므로 enableSortByDeadline()enableSortByPriority()도 정지 함수로 만들어야 합니다.
  • updateData()에서 받은 현재 UserPreferences를 사용하여 새 정렬 순서를 구성합니다.
  • UserPreferences 객체를 빌더로 변환하고 새 정렬 순서를 설정한 다음 preferences를 다시 빌드하여 UserPreferences 객체를 업데이트합니다.

enableSortByDeadline()은 다음과 같이 구현됩니다. enableSortByPriority()는 이후에 직접 변경해 보세요.

suspend fun enableSortByDeadline(enable: Boolean) {
    // updateData handles data transactionally, ensuring that if the sort is updated at the same
    // time from another thread, we won't have conflicts
    dataStore.updateData { preferences ->
        val currentOrder = preferences.sortOrder
        val newSortOrder =
            if (enable) {
                if (currentOrder == SortOrder.BY_PRIORITY) {
                    SortOrder.BY_DEADLINE_AND_PRIORITY
                } else {
                    SortOrder.BY_DEADLINE
                }
            } else {
                if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
                    SortOrder.BY_PRIORITY
                } else {
                    SortOrder.NONE
                }
            }
        preferences.toBuilder().setSortOrder(newSortOrder).build()
    }
}

이제 context 생성자 매개변수와 SharedPreferences의 모든 사용을 삭제할 수 있습니다.

9. UserPreferencesRepository를 사용하도록 TasksViewModel 업데이트

이제 UserPreferencesRepository가 Datastore에 show_completedsort_order를 모두 저장하고 Flow<UserPreferences>를 노출합니다. 이러한 요소를 사용하도록 TasksViewModel을 업데이트합니다.

showCompletedFlowsortOrderFlow를 삭제하고, userPreferencesRepository.userPreferencesFlow로 초기화되는 userPreferencesFlow라는 값을 만듭니다.

private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow

tasksUiModelFlow 생성 시 showCompletedFlowsortOrderFlowuserPreferencesFlow로 대체합니다. 그에 따라 매개변수를 대체합니다.

filterSortTasks를 호출할 때 userPreferencesshowCompletedsortOrder를 전달합니다. 코드는 다음과 같이 표시됩니다.

private val tasksUiModelFlow = combine(
        repository.tasks,
        userPreferencesFlow
    ) { tasks: List<Task>, userPreferences: UserPreferences ->
        return@combine TasksUiModel(
            tasks = filterSortTasks(
                tasks,
                userPreferences.showCompleted,
                userPreferences.sortOrder
            ),
            showCompleted = userPreferences.showCompleted,
            sortOrder = userPreferences.sortOrder
        )
    }

이제 showCompletedTasks() 함수가 userPreferencesRepository.updateShowCompleted()를 호출하도록 업데이트됩니다. 이 함수는 정지 함수이므로 viewModelScope에서 새 코루틴을 만드세요.

fun showCompletedTasks(show: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.updateShowCompleted(show)
    }
}

이제 userPreferencesRepository 함수인 enableSortByDeadline()enableSortByPriority()가 정지 함수이므로 viewModelScope에서 실행되는 새 코루틴에서도 호출되어야 합니다.

fun enableSortByDeadline(enable: Boolean) {
    viewModelScope.launch {
       userPreferencesRepository.enableSortByDeadline(enable)
    }
}

fun enableSortByPriority(enable: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.enableSortByPriority(enable)
    }
}

UserPreferencesRepository 지우기

더 이상 필요하지 않은 필드와 메서드를 삭제하겠습니다. 다음 항목을 삭제할 수 있습니다.

  • _sortOrderFlow
  • sortOrderFlow
  • updateSortOrder()
  • private val sortOrder: SortOrder
  • private val sharedPreferences

이제 앱이 성공적으로 컴파일됩니다. 이제 앱을 실행하여 show_completedsort_order 플래그가 올바르게 저장되었는지 확인합니다.

Codelab 저장소의 proto_datastore 분기를 확인하여 변경사항을 비교하세요.

10. 요약정리

Proto Datastore로의 이전 작업이 끝났으므로 배운 내용을 요약해 보겠습니다.

  • SharedPreferences는 UI 스레드에서 호출하기에 안전해 보일 수 있는 동기 API가 있고, 오류 신호를 보내는 메커니즘이 없고, 트랜잭션 API가 없다는 등 일련의 단점이 있습니다.
  • SharedPreferences를 대체하는 Datastore는 API의 거의 모든 단점을 해결합니다.
  • Datastore는 Kotlin 코루틴과 Flow를 사용하는 완전 비동기 API가 있으며, 데이터 이전을 처리하고, 데이터 일관성을 보장하고, 데이터 손상을 처리합니다.