DataStore   Android Jetpack의 구성요소

Jetpack Datastore는 프로토콜 버퍼를 사용하여 키-값 쌍 또는 유형이 지정된 객체를 저장할 수 있는 데이터 저장소 솔루션입니다. Datastore는 Kotlin 코루틴 및 Flow를 사용하여 비동기적이고 일관된 트랜잭션 방식으로 데이터를 저장합니다.

현재 SharedPreferences를 사용하여 데이터를 저장하고 있다면 대신 Datastore로 이전하는 것이 좋습니다.

Preferences DataStore 및 Proto DataStore

Datastore는 Preferences Datastore와 Proto Datastore라는 두 가지 구현을 제공합니다.

  • Preferences DataStore는 키를 사용하여 데이터를 저장하고 데이터에 액세스합니다. 이 구현은 유형 안전성을 제공하지 않으며 사전 정의된 스키마가 필요하지 않습니다.
  • Proto Datastore는 맞춤 데이터 유형의 인스턴스로 데이터를 저장합니다. 이 구현은 유형 안전성을 제공하며 프로토콜 버퍼를 사용하여 스키마를 정의해야 합니다.

DataStore를 올바르게 사용

DataStore를 올바르게 사용하려면 항상 다음 규칙에 유의하세요.

  1. 같은 프로세스에서 특정 파일의 DataStore 인스턴스를 두 개 이상 만들지 않습니다. 이렇게 하면 모든 DataStore 기능이 중단될 수 있습니다. 동일한 프로세스에서 특정 파일의 DataStore가 여러 개 활성화되어 있다면 데이터를 읽거나 업데이트할 때 DataStore가 IllegalStateException을 발생시킵니다.

  2. DataStore의 일반 유형은 변경 불가능해야 합니다. DataStore에 사용된 유형을 변경하면 DataStore가 제공하는 모든 보장이 무효화되고 잠재적으로 심각하고 포착하기 어려운 버그가 발생할 수 있습니다. 불변성을 보장하고 간단한 API와 효율적인 직렬화를 제공하는 프로토콜 버퍼를 사용하는 것이 좋습니다.

  3. 동일한 파일에서 SingleProcessDataStoreMultiProcessDataStore를 함께 사용하지 않습니다. 둘 이상의 프로세스에서 DataStore에 액세스하려면 항상 MultiProcessDataStore를 사용하세요.

설정

앱에서 Jetpack Datastore를 사용하려면 사용할 구현에 따라 다음을 Gradle 파일에 추가하세요.

Preferences DataStore

Groovy

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation "androidx.datastore:datastore-preferences:1.1.1"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.1"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.1"
    }
    

Kotlin

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.1")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.1.1")
    }
    

Proto DataStore

Groovy

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation "androidx.datastore:datastore:1.1.1"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.1.1"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.1"
    }
    

Kotlin

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.1")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.1.1")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.1.1")
    }
    

Preferences Datastore로 키-값 쌍 저장

Preferences DataStore 구현은 DataStore 클래스와 Preferences 클래스를 사용하여 간단한 키-값 쌍을 디스크에 유지합니다.

Preferences Datastore 만들기

preferencesDataStore로 만든 속성 위임을 사용하여 DataStore<Preferences>의 인스턴스를 만듭니다. kotlin 파일의 최상위 수준에서 인스턴스를 한 번 호출한 후 애플리케이션의 나머지 부분에서는 이 속성을 통해 인스턴스에 액세스합니다. 이렇게 하면 더 간편하게 DataStore를 싱글톤으로 유지할 수 있습니다. 또는 RxJava를 사용하는 경우 RxPreferenceDataStoreBuilder를 사용합니다. 필수 name 매개변수는 Preferences Datastore의 이름입니다.

Kotlin

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Java

RxDataStore<Preferences> dataStore =
  new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();

Preferences Datastore에서 읽기

Preferences Datastore는 사전 정의된 스키마를 사용하지 않으므로 DataStore<Preferences> 인스턴스에 저장해야 하는 각 값의 키를 정의하려면 상응하는 키 유형 함수를 사용해야 합니다. 예를 들어 int 값의 키를 정의하려면 intPreferencesKey()를 사용합니다. 그런 다음 DataStore.data 속성을 사용하여 Flow를 사용한 적절한 저장 값을 노출합니다.

Kotlin

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

Java

Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter");

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));

Preferences DataStore에 쓰기

Preferences Datastore는 DataStore의 데이터를 트랜잭션 방식으로 업데이트하는 edit() 함수를 제공합니다. 함수의 transform 매개변수는 필요에 따라 값을 업데이트할 수 있는 코드 블록을 허용합니다. 변환 블록의 모든 코드는 단일 트랜잭션으로 취급됩니다.

Kotlin

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

Java

Single<Preferences> updateResult =  dataStore.updateDataAsync(prefsIn -> {
  MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
  Integer currentInt = prefsIn.get(INTEGER_KEY);
  mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
  return Single.just(mutablePreferences);
});
// The update is completed once updateResult is completed.

유형이 지정된 객체를 Proto Datastore로 저장

Proto Datastore 구현은 Datastore 및 프로토콜 버퍼를 사용하여 유형이 지정된 객체를 디스크에 유지합니다.

스키마 정의

Proto Datastore를 사용하려면 app/src/main/proto/ 디렉터리의 proto 파일에 사전 정의된 스키마가 있어야 합니다. 사전 정의된 스키마는 Proto Datastore에 유지하는 객체의 유형을 정의합니다. proto 스키마를 정의하는 방법에 관한 자세한 내용은 protobuf 언어 가이드를 참고하세요.

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

Proto Datastore 만들기

유형이 지정된 객체를 저장할 Proto DataStore를 만드는 작업은 두 단계로 이루어집니다.

  1. Serializer<T>를 구현하는 클래스를 정의합니다. 여기서 T는 proto 파일에 정의된 유형입니다. serializer 클래스는 데이터 유형을 읽고 쓰는 방법을 Datastore에 알립니다. 아직 파일이 생성되지 않은 경우 사용할 serializer의 기본값을 포함해야 합니다.
  2. dataStore로 만든 속성 위임을 사용하여 DataStore<T>의 인스턴스를 만듭니다. 여기서 T는 proto 파일에 정의된 유형입니다. kotlin 파일의 최상위 수준에서 인스턴스를 한 번 호출한 후 애플리케이션의 나머지 부분에서는 이 속성을 통해 인스턴스에 액세스합니다. filename 매개변수는 데이터를 저장하는 데 사용할 파일을 Datastore에 알리고 serializer 매개변수는 1단계에서 정의한 serializer 클래스 이름을 Datastore에 알립니다.

Kotlin

object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()

  override suspend fun readFrom(input: InputStream): Settings {
    try {
      return Settings.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException("Cannot read proto.", exception)
    }
  }

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

val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName = "settings.pb",
  serializer = SettingsSerializer
)

Java

private static class SettingsSerializer implements Serializer<Settings> {
  @Override
  public Settings getDefaultValue() {
    Settings.getDefaultInstance();
  }

  @Override
  public Settings readFrom(@NotNull InputStream input) {
    try {
      return Settings.parseFrom(input);
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException(“Cannot read proto.”, exception);
    }
  }

  @Override
  public void writeTo(Settings t, @NotNull OutputStream output) {
    t.writeTo(output);
  }
}

RxDataStore<Byte> dataStore =
    new RxDataStoreBuilder<Byte>(context, /* fileName= */ "settings.pb", new SettingsSerializer()).build();

Proto Datastore에서 읽기

DataStore.data를 사용하여 저장된 객체에서 적절한 속성의 Flow를 노출합니다.

Kotlin

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }

Java

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(settings -> settings.getExampleCounter());

Proto DataStore에 쓰기

Proto Datastore는 저장된 객체를 트랜잭션 방식으로 업데이트하는 updateData() 함수를 제공합니다. updateData()는 데이터의 현재 상태를 데이터 유형의 인스턴스로 제공하고 원자적 읽기-쓰기-수정 작업을 통해 트랜잭션 방식으로 데이터를 업데이트합니다.

Kotlin

suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}

Java

Single<Settings> updateResult =
  dataStore.updateDataAsync(currentSettings ->
    Single.just(
      currentSettings.toBuilder()
        .setExampleCounter(currentSettings.getExampleCounter() + 1)
        .build()));

동기 코드에서 Datastore 사용

DataStore의 주요 이점 중 하나는 비동기 API이지만 주변 코드를 비동기로 변경하는 것이 항상 가능하지는 않을 수도 있습니다. 동기 디스크 I/O를 사용하는 기존 코드베이스로 작업하거나 비동기 API를 제공하지 않는 종속 항목이 있다면 이러한 상황이 발생할 수 있습니다.

Kotlin 코루틴은 runBlocking() 코루틴 빌더를 제공하여 동기 코드와 비동기 코드 간의 격차를 해소합니다. runBlocking()을 사용하여 Datastore에서 데이터를 동기식으로 읽을 수 있습니다. RxJava는 Flowable에서 차단 메서드를 제공합니다. 다음 코드는 Datastore가 데이터를 반환할 때까지 호출 스레드를 차단합니다.

Kotlin

val exampleData = runBlocking { context.dataStore.data.first() }

Java

Settings settings = dataStore.data().blockingFirst();

UI 스레드에서 동기 I/O 작업을 실행하면 ANR 또는 UI 버벅거림이 발생할 수 있습니다. Datastore에서 데이터를 비동기식으로 미리 로드하여 이 문제를 완화하세요.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

Java

dataStore.data().first().subscribe();

이렇게 하면 Datastore가 비동기식으로 데이터를 읽고 메모리에 캐시합니다. 초기 읽기가 완료되면 이후 runBlocking()을 사용한 동기 읽기가 더 빠를 수도 있고 디스크 I/O 작업을 완전히 방지할 수도 있습니다.

멀티 프로세스 코드에서 DataStore 사용

단일 프로세스 내에서와 동일한 데이터 일관성을 보장하는 여러 프로세스에서 동일한 데이터에 액세스하도록 DataStore를 구성할 수 있습니다. 특히 DataStore는 다음을 보장합니다.

  • 읽기는 디스크에 유지된 데이터만 반환합니다.
  • 쓰기 후 읽기 일관성을 제공합니다.
  • 쓰기는 직렬화됩니다.
  • 읽기가 쓰기에 의해 차단되지 않습니다.

서비스와 활동이 있는 샘플 애플리케이션을 생각해 보세요.

  1. 서비스는 별도의 프로세스에서 실행되며 DataStore를 주기적으로 업데이트합니다.

    <service
      android:name=".MyService"
      android:process=":my_process_id" />
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
          scope.launch {
              while(isActive) {
                  dataStore.updateData {
                      Settings(lastUpdate = System.currentTimeMillis())
                  }
                  delay(1000)
              }
          }
    }
    
  2. 앱에서 이러한 변경사항을 수집하고 UI를 업데이트하지만

    val settings: Settings by dataStore.data.collectAsState()
    Text(
      text = "Last updated: $${settings.timestamp}",
    )
    

여러 프로세스에서 DataStore를 사용할 수 있으려면 MultiProcessDataStoreFactory를 사용하여 DataStore 객체를 구성해야 합니다.

val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   }
)

serializer는 데이터 유형을 읽고 쓰는 방법을 DataStore에 알립니다. 아직 파일이 생성되지 않은 경우 사용할 serializer의 기본값을 포함해야 합니다. 다음은 kotlinx.serialization을 사용하는 구현의 예입니다.

@Serializable
data class Settings(
   val lastUpdate: Long
)

@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {

   override val defaultValue = Settings(lastUpdate = 0)

   override suspend fun readFrom(input: InputStream): Timer =
       try {
           Json.decodeFromString(
               Settings.serializer(), input.readBytes().decodeToString()
           )
       } catch (serialization: SerializationException) {
           throw CorruptionException("Unable to read Settings", serialization)
       }

   override suspend fun writeTo(t: Settings, output: OutputStream) {
       output.write(
           Json.encodeToString(Settings.serializer(), t)
               .encodeToByteArray()
       )
   }
}

Hilt 종속 항목 삽입을 사용하여 DataStore 인스턴스가 프로세스별로 고유하도록 할 수 있습니다.

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

파일 손상 처리

드물지만 DataStore의 영구 디스크 파일이 손상될 수 있습니다. 기본적으로 DataStore는 손상된 경우 자동으로 복구되지 않으며 DataStore에서 읽으려고 하면 시스템에서 CorruptionException이 발생합니다.

DataStore는 이러한 시나리오에서 원활하게 복구하고 예외가 발생하지 않도록 하는 데 도움이 되는 손상 핸들러 API를 제공합니다. 구성된 경우 손상 핸들러는 손상된 파일을 사전 정의된 기본값이 포함된 새 파일로 대체합니다.

이 핸들러를 설정하려면 by dataStore() 또는 DataStoreFactory 팩토리 메서드에서 DataStore 인스턴스를 만들 때 corruptionHandler를 제공합니다.

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

의견 보내기

다음 리소스를 통해 의견을 보내고 아이디어를 공유해 주세요.

Issue Tracker
버그를 수정할 수 있도록 문제를 신고해 주세요.

추가 리소스

Jetpack DataStore에 관한 자세한 내용은 다음 추가 리소스를 참고하세요.

샘플

블로그

Codelab