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 อย่างถูกต้อง

  1. อย่าสร้างอินสแตนซ์ DataStore มากกว่า 1 รายการสําหรับไฟล์หนึ่งๆ ในกระบวนการเดียวกัน การทำเช่นนี้อาจทำให้ฟังก์ชันการทำงานของ DataStore ทั้งหมดใช้งานไม่ได้ หากมี DataStore หลายรายการที่ใช้งานอยู่สําหรับไฟล์หนึ่งๆ ในกระบวนการเดียวกัน DataStore จะแสดง IllegalStateException เมื่ออ่านหรืออัปเดตข้อมูล

  2. ประเภททั่วไปของ DataStore ต้องเป็นแบบคงที่ การเปลี่ยนประเภทที่ใช้ใน DataStore จะลบล้างการรับประกันทั้งหมดที่ DataStore มีให้และสร้างข้อบกพร่องที่อาจร้ายแรงและตรวจจับได้ยาก เราขอแนะนําอย่างยิ่งให้คุณใช้ Protocol Buffers ซึ่งรับประกันความคงที่ของข้อมูล, API ที่ใช้งานง่าย และการแปลงเป็นอนุกรมที่มีประสิทธิภาพ

  3. อย่าใช้ SingleProcessDataStore และ MultiProcessDataStore ร่วมกันในไฟล์เดียวกัน หากคุณต้องการเข้าถึง DataStore จากกระบวนการมากกว่า 1 รายการ ให้ใช้ MultiProcessDataStore เสมอ

ตั้งค่า

หากต้องการใช้ Jetpack DataStore ในแอป ให้เพิ่มข้อมูลต่อไปนี้ลงในไฟล์ Gradle โดยขึ้นอยู่กับการใช้งานที่ต้องการใช้

Preferences DataStore

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

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

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

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.2"
    }
    
    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.2")

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

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

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

Proto DataStore

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

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

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

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.2"
    }
    
    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.2")

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

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

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

จัดเก็บคู่คีย์-ค่าด้วย Preferences DataStore

การใช้งาน Preferences DataStore ใช้คลาส DataStore และ Preferences เพื่อเก็บคู่คีย์-ค่าแบบง่ายไว้ในดิสก์

สร้าง Datastore สำหรับค่ากําหนด

ใช้ตัวรับมอบสิทธิ์พร็อพเพอร์ตี้ที่สร้างโดย preferencesDataStore เพื่อสร้างอินสแตนซ์ของ DataStore<Preferences> เรียกใช้เพียงครั้งเดียวที่ระดับบนสุดของไฟล์ Kotlin และเข้าถึงผ่านพร็อพเพอร์ตี้นี้ในแอปพลิเคชันส่วนที่เหลือ ซึ่งจะช่วยให้คุณเก็บ DataStore เป็นค่าเดี่ยวได้ง่ายขึ้น หรือใช้ RxPreferenceDataStoreBuilder หากใช้ RxJava พารามิเตอร์ name ที่ต้องระบุคือชื่อของ Preferences DataStore

KotlinJava
// 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

KotlinJava
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 มีฟังก์ชัน edit() ที่อัปเดตข้อมูลใน DataStore แบบธุรกรรม พารามิเตอร์ transform ของฟังก์ชันจะยอมรับบล็อกโค้ดที่คุณอัปเดตค่าได้ตามต้องการ ระบบจะถือว่าโค้ดทั้งหมดในบล็อกการเปลี่ยนรูปแบบเป็นธุรกรรมเดียว

KotlinJava
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 และ 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 ขั้นตอนดังนี้

  1. กำหนดคลาสที่ใช้ Serializer<T> โดยที่ T คือประเภทที่กําหนดไว้ในไฟล์โปรโต คลาส Serializer นี้จะบอก DataStore ว่าจะอ่านและเขียนประเภทข้อมูลของคุณอย่างไร ตรวจสอบว่าคุณได้ระบุค่าเริ่มต้นสำหรับตัวจัดรูปแบบที่จะใช้หากยังไม่มีการสร้างไฟล์
  2. ใช้ตัวรับมอบสิทธิ์พร็อพเพอร์ตี้ที่สร้างโดย dataStore เพื่อสร้างอินสแตนซ์ของ DataStore<T> โดยที่ T คือประเภทที่กําหนดไว้ในไฟล์โปรโต เรียกใช้สิ่งนี้เพียงครั้งเดียวที่ระดับบนสุดของไฟล์ Kotlin และเข้าถึงผ่านพร็อพเพอร์ตี้นี้ซึ่งจะมอบสิทธิ์ให้ส่วนที่เหลือของแอป พารามิเตอร์ filename จะบอก DataStore ว่าต้องใช้ไฟล์ใดเพื่อจัดเก็บข้อมูล และพารามิเตอร์ serializer จะบอก DataStore ชื่อของคลาส Serializer ที่กําหนดไว้ในขั้นตอนที่ 1
KotlinJava
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 ของพร็อพเพอร์ตี้ที่เหมาะสมจากออบเจ็กต์ที่เก็บไว้

KotlinJava
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() แสดงสถานะปัจจุบันของข้อมูลเป็นอินสแตนซ์ของประเภทข้อมูลและอัปเดตข้อมูลแบบธุรกรรมในการดำเนินการแบบอ่าน-เขียน-แก้ไขแบบอะตอม

KotlinJava
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 ของดิสก์แบบซิงค์ หรือหากคุณมีทรัพยากร Dependency ที่ไม่มี API แบบแอซิงโครนัส

โคโริวทีนของ Kotlin มีเครื่องมือสร้างโคโริวทีน runBlocking() เพื่อช่วยปิดช่องว่างระหว่างโค้ดแบบซิงค์และแบบไม่ซิงค์ คุณสามารถใช้ runBlocking() เพื่ออ่านข้อมูลจาก DataStore แบบซิงค์ได้ RxJava มีวิธีการบล็อกใน Flowable โค้ดต่อไปนี้จะบล็อกเธรดการเรียกใช้จนกว่า Datastore จะแสดงผลข้อมูล

KotlinJava
val exampleData = runBlocking { context.dataStore.data.first() }
Settings settings = dataStore.data().blockingFirst();

การดำเนินการ I/O แบบซิงค์ในเธรด UI อาจทำให้เกิด ANR หรือ UI กระตุก คุณสามารถลดปัญหาเหล่านี้ได้โดยโหลดข้อมูลล่วงหน้าแบบไม่พร้อมกันจาก DataStore ดังนี้

KotlinJava
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 จะรับประกันสิ่งต่อไปนี้

  • การอ่านจะแสดงเฉพาะข้อมูลที่เก็บถาวรไว้ในดิสก์
  • ความสอดคล้องแบบ Read-after-write
  • การเขียนจะได้รับการอนุกรม
  • การอ่านจะไม่ถูกบล็อกโดยการเขียน

ลองดูตัวอย่างแอปพลิเคชันที่มีบริการและกิจกรรม

  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 ในกระบวนการต่างๆ คุณต้องสร้างออบเจ็กต์ 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

ไม่มีคำแนะนำในขณะนี้

ลองบัญชี Google