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를 올바르게 사용하려면 항상 다음 규칙에 유의하세요.
같은 프로세스에서 특정 파일의
DataStore
인스턴스를 두 개 이상 만들지 않습니다. 이렇게 하면 모든 DataStore 기능이 중단될 수 있습니다. 동일한 프로세스에서 특정 파일의 DataStore가 여러 개 활성화되어 있다면 데이터를 읽거나 업데이트할 때 DataStore가IllegalStateException
을 발생시킵니다.DataStore의 일반 유형은
변경 불가능해야 합니다. DataStore에 사용된 유형을 변경하면 DataStore가 제공하는 모든 보장이 무효화되고 잠재적으로 심각하고 포착하기 어려운 버그가 발생할 수 있습니다. 불변성을 보장하고 간단한 API와 효율적인 직렬화를 제공하는 프로토콜 버퍼를 사용하는 것이 좋습니다.동일한 파일에서
SingleProcessDataStore
와MultiProcessDataStore
를 함께 사용하지 않습니다. 둘 이상의 프로세스에서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를 만드는 작업은 두 단계로 이루어집니다.
Serializer<T>
를 구현하는 클래스를 정의합니다. 여기서T
는 proto 파일에 정의된 유형입니다. serializer 클래스는 데이터 유형을 읽고 쓰는 방법을 Datastore에 알립니다. 아직 파일이 생성되지 않은 경우 사용할 serializer의 기본값을 포함해야 합니다.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는 다음을 보장합니다.
- 읽기는 디스크에 유지된 데이터만 반환합니다.
- 쓰기 후 읽기 일관성을 제공합니다.
- 쓰기는 직렬화됩니다.
- 읽기가 쓰기에 의해 차단되지 않습니다.
서비스와 활동이 있는 샘플 애플리케이션을 생각해 보세요.
서비스는 별도의 프로세스에서 실행되며 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) } } }
앱에서 이러한 변경사항을 수집하고 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
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- 페이징 데이터 로드 및 표시
- LiveData 개요
- 레이아웃 및 바인딩 수식