Datastore 튜토리얼

Datastore

Jetpack Datastore은 키-값 쌍(Preferences Datastore) 또는 유형(Proto Datastore)이 지정된 객체를 저장할수 있는 데이터 솔루션입니다.

이전에 쓰던 SharedPreference를 Datastore로 이전하는걸 추천합니다.

SharedPreference는 저장시 UI Thread 동작을 멈춥니다.

그 외로도 Datastore을 사용하게 된다면 얻을수 있는 이점들이 많습니다.

  • 비동기 처리
  • 에러핸들링
  • 타입 safety
  • 데이터 일관성

Datastore는 간단한 값을 저장할때 쓰기 좋습니다.

문서에도 나와있는데, 복잡한 대규모 데이터 세트나 부분 업데이트, 참조무결성을 원한다면 (말그대로 데이터베이스) ROOM을 사용하는것이 좋습니다.

데이터스토어는 두가지 저장방식을 제공합니다.

  1. Preference DataStore
  2. Proto DataStore

두가지 모두 알아보겠습니다.

1. Preference DataStore

키-값으로 저장하는 데이터스토어입니다.

Shared Preference와 비슷합니다.

gradle에 datastore-preferences를 추가합니다.

dependencies {
        implementation("androidx.datastore:datastore-preferences:1.0.0")
}

Datastore 인스턴스를 코틀린파일 최상위수준에서 한번 호출해주어야 전역적으로 인스턴스에 액세스할수 있습니다.

아래와 같이 구현하면 쉽게 Datastore를 싱글톤으로 유지할수있습니다.

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

저는 이 코드를 MainActivity.kt 상단에 작성하였습니다.

Datastore에 사용할 키-값을 정합니다.

적절한 타입유형과 키 이름을 지정합니다.

  • booleanPreferencesKey
  • doublePreferencesKey
  • emptyPreferences
  • floatPreferencesKey
  • 등등... 참고 top-level-functions
val EXAMPLE_COUNT = intPreferencesKey("example_count")

EXAMPLE_COUNT는 preferences를 접근할때 사용하는 Preferences.Key 입니다.

데이터 읽기

val exampleCounterFlow: Flow<Int> = this.dataStore.data.map{preference->
	preference[EXAMPLE_COUNT] ?: 0
}

this.dataStore.data의 flow를 생성하고 이를 구독하는 value를 만듭니다.

저는 컴포즈를 사용하고있기 때문에 컴포즈 기준으로 설명드리겠습니다.

val counter = exampleCounterFlow.collectAsState(initial = 0)

counter 변수는 exampleCounterFlow를 구독하고 있습니다.

counter.value로 저장된 값을 사용할수 있습니다.

에러핸들링

flow에서 data에 대한 에러 핸들링이가능합니다.

val exampleCounterFlow: Flow<Int> = this.dataStore.data
.catch( exception ->
	if (exception is IOException) {
		emit(emptyPreferences())
	} else {
		throw exception
	}
.map{preference->
	preference[EXAMPLE_COUNT] ?: 0
}

데이터를 읽는동안 오류가 발생하면 IOException을 발생시킵니다. IOException은 emptyPreferences()를 방출하여 처리할수 있고, 다른 예외상황은 throw를 시키는 편이 좋습니다.

데이터 쓰기

데이터를 쓰는 방법으로는 DataStore.edit(transform: suspend (MutablePreferences) -> Unit)을 제공합니다. 이 함수는 트랜잭션 장식으로 상태를 업데이트할수있는 transform 블록을 허용합니다.

settings의 EXAMPLE_COUNT 값을 증가시키는 예제입니다.

suspend fun increaseCounter() {
    this.dataStore.edit { settings ->
        val currentCounterValue = settings[EXAMPLE_COUNT] ?: 0
        settings[EXAMPLE_COUNT] = currentCounterValue + 1
    }
}

2. Proto

Proto 버퍼 데이터구조를 가집니다.

복잡한 구조의 데이터를 저장합니다 (enum, list)

Proto Datastore를 사용해야 할때

  • 타입 오브젝트를 사용해야 하는경우
  • 효과적인 에러핸들링
  • 코루틴/flow 비동기처리
  • 빠른 데이터 마이그레이션

Protocol Buffers란?

  • 구글에서 개발한 다양한 언어와 플랫폼을 지원하는 구조화된 데이터를 직렬화하는 매커니즘
  • XML보다 직렬화 속도가 빠르고 크기도 작다
  • 쉽게 데이터를 읽을수 있다.

proto datastore 사용해보기

gradle에 datastore을 추가합니다.

implementation("androidx.datastore:datastore:1.0.0")

Proto datastore을 구현하려면 app/src/main/proto/ 디렉터리에 proto 파일에 사전정의된 스키마가 있어야합니다.

https://developers.google.com/protocol-buffers/docs/proto3?hl=ko

proto 스키마에 대해서는 위 링크를 참고해주세요.

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message SettingsPreferences {
  int32 example_counter = 1;
}

예제를 따라가다 보니 막히는 부분이 proto 스키마를 만들었으나 Serializer로 생성해주지 않았다.

https://developer.android.com/codelabs/android-proto-datastore?hl=ko#4 문서를 참고해보니 Serializer를 생성하려면 gradle 을 조금 수정해주어야 한다.

plugins {
    ...
    id "com.google.protobuf" version "0.8.12"
}

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

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.10.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'
                }
            }
        }
    }
}

빌드를 해주면 message에 작성해놓은 SettingsPreferences 가 자동 생성됩니다.

data패키지에 Serializer를 구현합니다.

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

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

이제 데이터스토어를 만들면 되는데 여기서 삽질을 좀 했습니다.

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

Context에 createDataStore 함수가 없습니다.

원인을 찾아보니 아래 릴리즈 로그에 createDataStore은 Context에서 삭제되었다고 나와

https://developer.android.com/jetpack/androidx/releases/datastore?hl=ko#1.0.0-alpha07

대신 아래와 같이 구현합니다.

val Context.dataProto: DataStore<SettingsPreferences> by dataStore<SettingsPreferences>(
    fileName = "settings.db",
    serializer = SettingsPreferencesSerializer,
)

값 업데이트 하기

suspend fun increaseProtoCounter() {
    this.dataProto.updateData { t: SettingsPreferences ->
        t.toBuilder().setExampleCounter(t.exampleCounter + 1).build()
    }
}

updateData 함수를 이용해 트랜잭션 방식으로 업데이트 합니다.

Datastore로 간단한 작은 데이터, 상태들을 저장할수 있음을 알수 있었습니다.

정말 간단하게 key-value만으로 저장한다면 Datastore Preference.

Object형식으로 크지 않은 데이터를 저장한다면 protocols buffer를 활용한 Datastore proto를 활용하는게 좋아보입니다.

Best Practice로는 Hilts 로 DI를 구현하라고 합니다.

https://www.youtube.com/watch?v=S10ci36lBJ4&list=PLWz5rJ2EKKc8to3Ere-ePuco69yBUmQ9C&index=5

Hilts를 공부해고 좀더 나은 구조로 바꿔보겠습니다.


참고

https://www.youtube.com/watch?v=9ws-cJzlJkU

https://developer.android.com/codelabs/android-proto-datastore?hl=ko#4

구글 프로토콜 버퍼 - https://bcho.tistory.com/1182

좋은 웹페이지 즐겨찾기