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
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.4" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.4" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.4" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.4" }
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.4") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.4") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.4") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.4") }
Proto DataStore
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.4" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.4" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.4" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.4" }
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.4") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.4") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.4") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.4") }
Preferences DataStore로 키-값 쌍 저장
Preferences DataStore 구현은 DataStore
클래스와 Preferences
클래스를 사용하여 간단한 키-값 쌍을 디스크에 유지합니다.
Preferences Datastore 만들기
preferencesDataStore
로 만든 속성 위임을 사용하여 DataStore<Preferences>
의 인스턴스를 만듭니다. kotlin 파일의 최상위 수준에서 인스턴스를 한 번 호출한 후 애플리케이션의 나머지 부분에서는 이 속성을 통해 인스턴스에 액세스합니다. 이렇게 하면 더 간편하게 DataStore
를 싱글톤으로 유지할 수 있습니다. 또는 RxJava를 사용하는 경우 RxPreferenceDataStoreBuilder
를 사용합니다. 필수 name
매개변수는 Preferences Datastore의 이름입니다.
// At the top level of your kotlin file: val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
RxDataStore<Preferences> dataStore = new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();
Preferences Datastore에서 읽기
Preferences Datastore는 사전 정의된 스키마를 사용하지 않으므로 DataStore<Preferences>
인스턴스에 저장해야 하는 각 값의 키를 정의하려면 상응하는 키 유형 함수를 사용해야 합니다. 예를 들어 int 값의 키를 정의하려면 intPreferencesKey()
를 사용합니다.
그런 다음 DataStore.data
속성을 사용하여 Flow
를 사용한 적절한 저장 값을 노출합니다.
val EXAMPLE_COUNTER = intPreferencesKey("example_counter") val exampleCounterFlow: Flow<Int> = context.dataStore.data .map { preferences -> // No type safety. preferences[EXAMPLE_COUNTER] ?: 0 }
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
매개변수는 필요에 따라 값을 업데이트할 수 있는 코드 블록을 허용합니다. 변환 블록의 모든 코드는 단일 트랜잭션으로 취급됩니다.
suspend fun incrementCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0 settings[EXAMPLE_COUNTER] = currentCounterValue + 1 } }
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.proto";
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에 알립니다.
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 )
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
를 노출합니다.
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data .map { settings -> // The exampleCounter property is generated from the proto schema. settings.exampleCounter }
Flowable<Integer> exampleCounterFlow = dataStore.data().map(settings -> settings.getExampleCounter());
Proto DataStore에 쓰기
Proto Datastore는 저장된 객체를 트랜잭션 방식으로 업데이트하는 updateData()
함수를 제공합니다. updateData()
는 데이터의 현재 상태를 데이터 유형의 인스턴스로 제공하고 원자적 읽기-쓰기-수정 작업을 통해 트랜잭션 방식으로 데이터를 업데이트합니다.
suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setExampleCounter(currentSettings.exampleCounter + 1) .build() } }
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가 데이터를 반환할 때까지 호출 스레드를 차단합니다.
val exampleData = runBlocking { context.dataStore.data.first() }
Settings settings = dataStore.data().blockingFirst();
UI 스레드에서 동기 I/O 작업을 실행하면 ANR 또는 UI 버벅거림이 발생할 수 있습니다. Datastore에서 데이터를 비동기식으로 미리 로드하여 이 문제를 완화하세요.
override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { context.dataStore.data.first() // You should also handle IOExceptions here. } }
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): Settings =
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에 관한 자세한 내용은 다음 추가 리소스를 참고하세요.
샘플
Learn how this app was designed and built in the design case study, architecture learning journey and modularization learning journey.
This is the repository for the Now in Android app. It is a work in progress 🚧.
Now in Android is a fully functionalNow in Android App
블로그
Codelab
추천 항목
페이징 데이터 로드 및 표시
Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.
LiveData 개요
LiveData를 사용하여 수명 주기를 인식하는 방식으로 데이터를 처리합니다.
레이아웃 및 바인딩 수식
Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.