DataStore   ส่วนหนึ่งของ Android Jetpack

ลองใช้ Kotlin Multiplatform
Kotlin Multiplatform ช่วยให้แชร์เลเยอร์ข้อมูลกับแพลตฟอร์มอื่นๆ ได้ ดูวิธีตั้งค่าและทำงานกับ DataStore ใน KMP

Jetpack DataStore เป็นโซลูชันพื้นที่เก็บข้อมูลที่ช่วยให้คุณจัดเก็บคู่คีย์-ค่าหรือออบเจ็กต์ที่มีการพิมพ์ด้วย Protocol Buffers DataStore ใช้โครูทีนและ Flow ของ Kotlin เพื่อจัดเก็บข้อมูลแบบไม่พร้อมกัน อย่างสม่ำเสมอ และแบบธุรกรรม

หากปัจจุบันคุณใช้ SharedPreferences เพื่อจัดเก็บข้อมูล ให้ลองย้ายข้อมูลไปยัง DataStore แทน

DataStore ของค่ากำหนดและ Proto DataStore

DataStore มีการใช้งาน 2 แบบ ได้แก่ Preferences DataStore และ Proto DataStore

  • DataStore ของค่ากําหนดจะจัดเก็บและเข้าถึงข้อมูลโดยใช้คีย์ การ ติดตั้งใช้งานนี้ไม่จำเป็นต้องมีสคีมาที่กำหนดไว้ล่วงหน้า และไม่มี ความปลอดภัยของประเภท
  • Proto DataStore จัดเก็บข้อมูลเป็นอินสแตนซ์ของประเภทข้อมูลที่กำหนดเอง การใช้งานนี้กำหนดให้คุณต้องกำหนดสคีมาโดยใช้บัฟเฟอร์ โปรโตคอล แต่จะให้ความปลอดภัย ของประเภท

การใช้ DataStore อย่างถูกต้อง

โปรดคำนึงถึงกฎต่อไปนี้เสมอเพื่อให้ใช้ DataStore ได้อย่างถูกต้อง

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

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

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

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

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

Kotlin

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

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

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

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

Proto DataStore

Groovy

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

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

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

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

Kotlin

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

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

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

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

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

การใช้งาน DataStore ของค่ากําหนดจะใช้คลาส DataStore และ Preferences เพื่อบันทึกคู่คีย์-ค่าอย่างง่ายลงในดิสก์

สร้าง 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 ไม่ได้ใช้สคีมาที่กำหนดไว้ล่วงหน้า คุณจึงต้องใช้ฟังก์ชันประเภทคีย์ที่เกี่ยวข้องเพื่อกำหนดคีย์สำหรับแต่ละค่าที่ต้องการจัดเก็บในอินสแตนซ์ 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 และบัฟเฟอร์โปรโตคอลเพื่อบันทึกออบเจ็กต์ที่มีการพิมพ์ลงในดิสก์

กำหนดสคีมา

Proto DataStore ต้องมีสคีมาที่กำหนดไว้ล่วงหน้าในไฟล์ Proto ในไดเรกทอรี app/src/main/proto/ สคีมานี้กำหนดประเภทของออบเจ็กต์ ที่คุณคงไว้ใน Proto DataStore ดูข้อมูลเพิ่มเติมเกี่ยวกับการกำหนดสคีมาโปรโต ได้ที่คำแนะนำเกี่ยวกับภาษา 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 เพื่อจัดเก็บออบเจ็กต์ที่มีการพิมพ์ มี 2 ขั้นตอนดังนี้

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

คุณสามารถใช้การแทรก 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 ของ Factory ดังนี้

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

แสดงความคิดเห็น

แชร์ความคิดเห็นและไอเดียกับเราผ่านแหล่งข้อมูลต่อไปนี้

เครื่องมือติดตามปัญหา
รายงานปัญหาเพื่อให้เราแก้ไขข้อบกพร่องได้

แหล่งข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับ Jetpack DataStore ได้จากแหล่งข้อมูลเพิ่มเติมต่อไปนี้

ตัวอย่าง

บล็อก

Codelabs