DataStore เป็นส่วนหนึ่งของ Android Jetpack
Jetpack DataStore เป็นโซลูชันพื้นที่เก็บข้อมูลที่ให้คุณจัดเก็บคู่คีย์-ค่าหรือออบเจ็กต์ที่มีการจัดประเภทด้วย protocol buffer Datastore ใช้โคโริวทีนและ Flow ของ Kotlin เพื่อจัดเก็บข้อมูลแบบไม่พร้อมกัน สอดคล้องกัน และเป็นแบบธุรกรรม
หากคุณกําลังใช้ SharedPreferences
เพื่อจัดเก็บข้อมูลอยู่ ให้ลองย้ายข้อมูลไปยัง DataStore แทน
Preferences DataStore และ Proto DataStore
Datastore มีการใช้งาน 2 แบบ ได้แก่ Preferences DataStore และ Proto DataStore
- Preferences DataStore จะจัดเก็บและเข้าถึงข้อมูลโดยใช้คีย์ การใช้งานนี้ไม่จำเป็นต้องใช้สคีมาที่กำหนดไว้ล่วงหน้า และไม่มีความปลอดภัยของประเภท
- Proto DataStore จัดเก็บข้อมูลเป็นอินสแตนซ์ของประเภทข้อมูลที่กําหนดเอง การใช้งานนี้กำหนดให้คุณกำหนดสคีมาโดยใช้ protocol buffer แต่ให้ความปลอดภัยของประเภท
การใช้ Datastore อย่างถูกต้อง
โปรดคำนึงถึงกฎต่อไปนี้เสมอเพื่อใช้ DataStore อย่างถูกต้อง
อย่าสร้างอินสแตนซ์
DataStore
มากกว่า 1 รายการสําหรับไฟล์หนึ่งๆ ในกระบวนการเดียวกัน การทำเช่นนี้อาจทำให้ฟังก์ชันการทำงานของ DataStore ทั้งหมดใช้งานไม่ได้ หากมี DataStore หลายรายการที่ใช้งานอยู่สําหรับไฟล์หนึ่งๆ ในกระบวนการเดียวกัน DataStore จะแสดงIllegalStateException
เมื่ออ่านหรืออัปเดตข้อมูลประเภททั่วไปของ DataStore
ต้องเป็นแบบคงที่ การเปลี่ยนประเภทที่ใช้ใน DataStore จะลบล้างการรับประกันทั้งหมดที่ DataStore มีให้และสร้างข้อบกพร่องที่อาจร้ายแรงและตรวจจับได้ยาก เราขอแนะนําอย่างยิ่งให้คุณใช้ Protocol Buffers ซึ่งรับประกันความคงที่ของข้อมูล, API ที่ใช้งานง่าย และการแปลงเป็นอนุกรมที่มีประสิทธิภาพอย่าใช้
SingleProcessDataStore
และMultiProcessDataStore
ร่วมกันในไฟล์เดียวกัน หากคุณต้องการเข้าถึงDataStore
จากกระบวนการมากกว่า 1 รายการ ให้ใช้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
เพื่อเก็บคู่คีย์-ค่าแบบง่ายไว้ในดิสก์
สร้าง Datastore สำหรับค่ากําหนด
ใช้ตัวรับมอบสิทธิ์พร็อพเพอร์ตี้ที่สร้างโดย preferencesDataStore
เพื่อสร้างอินสแตนซ์ของ DataStore<Preferences>
เรียกใช้เพียงครั้งเดียวที่ระดับบนสุดของไฟล์ Kotlin และเข้าถึงผ่านพร็อพเพอร์ตี้นี้ในแอปพลิเคชันส่วนที่เหลือ ซึ่งจะช่วยให้คุณเก็บ DataStore
เป็นค่าเดี่ยวได้ง่ายขึ้น หรือใช้ RxPreferenceDataStoreBuilder
หากใช้ RxJava พารามิเตอร์ 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 มีฟังก์ชัน edit()
ที่อัปเดตข้อมูลใน DataStore
แบบธุรกรรม พารามิเตอร์ 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 และ protocol buffer เพื่อเก็บออบเจ็กต์ที่มีการจัดประเภทไว้ในดิสก์
กําหนดสคีมา
Proto DataStore ต้องใช้สคีมาที่กำหนดไว้ล่วงหน้าในไฟล์ Proto ในไดเรกทอรี app/src/main/proto/
สคีมานี้จะกําหนดประเภทของออบเจ็กต์ที่คุณเก็บไว้ใน Proto DataStore ดูข้อมูลเพิ่มเติมเกี่ยวกับการกําหนดสคีมาโปรโตได้ที่คู่มือภาษา protobuf
syntax = "proto3";
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
สร้าง Proto DataStore
การสร้าง Proto DataStore เพื่อจัดเก็บออบเจ็กต์ที่มีประเภทมี 2 ขั้นตอนดังนี้
- กำหนดคลาสที่ใช้
Serializer<T>
โดยที่T
คือประเภทที่กําหนดไว้ในไฟล์โปรโต คลาส Serializer นี้จะบอก DataStore ว่าจะอ่านและเขียนประเภทข้อมูลของคุณอย่างไร ตรวจสอบว่าคุณได้ระบุค่าเริ่มต้นสำหรับตัวจัดรูปแบบที่จะใช้หากยังไม่มีการสร้างไฟล์ - ใช้ตัวรับมอบสิทธิ์พร็อพเพอร์ตี้ที่สร้างโดย
dataStore
เพื่อสร้างอินสแตนซ์ของDataStore<T>
โดยที่T
คือประเภทที่กําหนดไว้ในไฟล์โปรโต เรียกใช้สิ่งนี้เพียงครั้งเดียวที่ระดับบนสุดของไฟล์ Kotlin และเข้าถึงผ่านพร็อพเพอร์ตี้นี้ซึ่งจะมอบสิทธิ์ให้ส่วนที่เหลือของแอป พารามิเตอร์filename
จะบอก DataStore ว่าต้องใช้ไฟล์ใดเพื่อจัดเก็บข้อมูล และพารามิเตอร์serializer
จะบอก DataStore ชื่อของคลาส Serializer ที่กําหนดไว้ในขั้นตอนที่ 1
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 ของดิสก์แบบซิงค์ หรือหากคุณมีทรัพยากร Dependency ที่ไม่มี API แบบแอซิงโครนัส
โคโริวทีนของ Kotlin มีเครื่องมือสร้างโคโริวทีน runBlocking()
เพื่อช่วยปิดช่องว่างระหว่างโค้ดแบบซิงค์และแบบไม่ซิงค์ คุณสามารถใช้ runBlocking()
เพื่ออ่านข้อมูลจาก DataStore แบบซิงค์ได้
RxJava มีวิธีการบล็อกใน Flowable
โค้ดต่อไปนี้จะบล็อกเธรดการเรียกใช้จนกว่า Datastore จะแสดงผลข้อมูล
Kotlin
val exampleData = runBlocking { context.dataStore.data.first() }
Java
Settings settings = dataStore.data().blockingFirst();
การดำเนินการ I/O แบบซิงค์ในเธรด UI อาจทำให้เกิด 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 จะรับประกันสิ่งต่อไปนี้
- การอ่านจะแสดงเฉพาะข้อมูลที่เก็บถาวรไว้ในดิสก์
- ความสอดคล้องแบบ Read-after-write
- การเขียนจะได้รับการอนุกรม
- การอ่านจะไม่ถูกบล็อกโดยการเขียน
ลองดูตัวอย่างแอปพลิเคชันที่มีบริการและกิจกรรม
บริการจะทํางานในกระบวนการแยกต่างหากและอัปเดต 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 ในกระบวนการต่างๆ คุณต้องสร้างออบเจ็กต์ DataStore โดยใช้ MultiProcessDataStoreFactory
val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
}
)
serializer
บอก DataStore ว่าจะอ่านและเขียนประเภทข้อมูลของคุณอย่างไร
ตรวจสอบว่าคุณได้ระบุค่าเริ่มต้นสำหรับโปรแกรมจัดรูปแบบที่จะใช้ในกรณีที่ยังไม่มีการสร้างไฟล์ ด้านล่างนี้คือตัวอย่างการใช้งานโดยใช้ 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()
)
}
}
คุณสามารถใช้การฉีด Dependency ของ Hilt เพื่อให้อินสแตนซ์ DataStore ของคุณไม่ซ้ำกันในแต่ละกระบวนการ ดังนี้
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
MultiProcessDataStoreFactory.create(...)
จัดการไฟล์ที่เสียหาย
ในบางกรณีที่พบไม่บ่อยนัก ไฟล์ถาวรบนดิสก์ของ DataStore อาจเสียหาย โดยค่าเริ่มต้น DataStore จะไม่กู้คืนจากความเสียหายโดยอัตโนมัติ และการพยายามอ่านจาก DataStore จะทําให้ระบบแสดงCorruptionException
DataStore มี API ตัวจัดการความเสียหายที่จะช่วยคุณกู้คืนได้อย่างราบรื่นในสถานการณ์เช่นนี้ และหลีกเลี่ยงการยกเว้น เมื่อกําหนดค่าแล้ว ตัวจัดการความเสียหายจะแทนที่ไฟล์ที่เสียหายด้วยไฟล์ใหม่ซึ่งมีค่าเริ่มต้นที่กําหนดไว้ล่วงหน้า
หากต้องการตั้งค่าตัวแฮนเดิลนี้ ให้ระบุ corruptionHandler
เมื่อสร้างอินสแตนซ์ DataStore ใน by dataStore()
หรือในเมธอด DataStoreFactory
val dataStore: DataStore<Settings> = DataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
},
corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)
แสดงความคิดเห็น
แชร์ความคิดเห็นและไอเดียกับเราผ่านแหล่งข้อมูลต่อไปนี้
- เครื่องมือติดตามปัญหา
- รายงานปัญหาเพื่อให้เราแก้ไขข้อบกพร่องได้
แหล่งข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับ Jetpack DataStore ได้ที่แหล่งข้อมูลเพิ่มเติมต่อไปนี้
ตัวอย่าง
บล็อก
Codelabs
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- โหลดและแสดงข้อมูลที่แบ่งหน้า
- ภาพรวม LiveData
- เลย์เอาต์และนิพจน์การเชื่อมโยง