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

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

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

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

  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