DataStore Bagian dari Android Jetpack.

Jetpack DataStore adalah solusi penyimpanan data yang memungkinkan Anda menyimpan key-value pair atau objek yang diketik dengan buffering protokol. DataStore menggunakan coroutine Kotlin dan Flow untuk menyimpan data secara asinkron, konsisten, dan transaksional.

Jika saat ini Anda menggunakan SharedPreferences untuk menyimpan data, sebaiknya bermigrasilah ke DataStore.

Preferences DataStore dan Proto DataStore

DataStore menyediakan dua implementasi yang berbeda: Preferences DataStore dan Proto DataStore.

  • Preferences DataStore menyimpan dan mengakses data menggunakan kunci. Implementasi ini tidak memerlukan skema yang telah ditetapkan sebelumnya, dan tidak memberikan keamanan jenis.
  • Proto DataStore menyimpan data sebagai instance jenis data kustom. Implementasi ini mengharuskan Anda untuk menentukan skema menggunakan buffering protokol, tetapi memberikan keamanan jenis.

Menggunakan DataStore dengan benar

Agar dapat menggunakan DataStore dengan benar, selalu perhatikan aturan berikut:

  1. Jangan pernah membuat lebih dari satu instance DataStore untuk file tertentu dengan proses yang sama. Tindakan ini dapat merusak semua fungsi DataStore. Jika ada beberapa DataStore yang aktif untuk file tertentu dalam proses yang sama, DataStore akan menampilkan IllegalStateException saat membaca atau memperbarui data.

  2. Jenis DataStore umum harus tidak dapat diubah. Mengubah jenis yang digunakan di DataStore akan membatalkan jaminan yang diberikan DataStore dan juga membuat bug serius yang berpotensi sulit ditemukan. Sangat disarankan agar Anda menggunakan buffering protokol yang memberikan jaminan immutabilitas, API sederhana, dan serialisasi yang lebih efisien.

  3. Jangan pernah menggabungkan penggunaan SingleProcessDataStore dan MultiProcessDataStore untuk file yang sama. Jika Anda ingin mengakses DataStore dari beberapa ini, selalu gunakan MultiProcessDataStore.

Penyiapan

Untuk menggunakan Jetpack DataStore di aplikasi, tambahkan berikut ini ke file Gradle Anda bergantung pada implementasi yang ingin digunakan:

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")
    }
    

Menyimpan key-value pair dengan Preferences DataStore

Implementasi Preferences DataStore menggunakan class DataStore dan Preferences untuk mempertahankan key-value pair sederhana ke disk.

Membuat Preferences DataStore

Gunakan delegasi properti yang dibuat oleh preferencesDataStore untuk membuat instance Datastore<Preferences>. Panggil sekali di tingkat teratas file kotlin, dan akses melalui properti ini di seluruh aplikasi Anda. Hal ini memudahkan Anda mempertahankan DataStore sebagai singleton. Atau, gunakan RxPreferenceDataStoreBuilder jika Anda menggunakan RxJava. Parameter name wajib adalah nama 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();

Membaca dari Preferences DataStore

Karena Preferences DataStore tidak menggunakan skema yang telah ditetapkan sebelumnya, Anda harus menggunakan fungsi jenis kunci yang sesuai untuk menentukan kunci untuk setiap nilai yang perlu disimpan dalam instance DataStore<Preferences>. Misalnya, untuk menentukan kunci nilai int, gunakan intPreferencesKey(). Selanjutnya, gunakan properti DataStore.data untuk mengekspos nilai tersimpan yang sesuai menggunakan 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));

Menulis ke Preferences DataStore

Preferences DataStore menyediakan fungsi edit() yang mengupdate data secara transaksional dalam DataStore. Parameter transform fungsi menerima blok kode tempat Anda dapat mengupdate nilai yang diperlukan. Semua kode dalam blok transformasi diperlakukan sebagai transaksi tunggal.

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.

Menyimpan objek yang diketik dengan Proto DataStore

Implementasi Proto DataStore menggunakan DataStore dan buffering protokol untuk mempertahankan objek yang diketik ke disk.

Menetapkan skema

Proto DataStore memerlukan skema yang telah ditetapkan sebelumnya dalam file proto di direktori app/src/main/proto/. Skema ini menetapkan jenis objek yang Anda pertahankan di Proto DataStore. Untuk mempelajari lebih lanjut penetapan skema proto, baca panduan bahasa protobuf.

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Membuat Proto DataStore

Ada dua langkah diperlukan untuk membuat Proto DataStore untuk menyimpan objek yang diketik:

  1. Tentukan class yang mengimplementasikan Serializer<T>, dengan T adalah jenis yang ditetapkan dalam file proto. Class penserialisasi ini memberi tahu DataStore cara membaca dan menulis jenis data Anda. Pastikan Anda menyertakan nilai default untuk serialisasi yang akan digunakan jika belum ada file yang dibuat.
  2. Gunakan delegasi properti yang dibuat oleh dataStore untuk membuat instance DataStore<T>, dengan T adalah jenis yang ditetapkan dalam file proto. Panggil ini sekali di tingkat teratas file kotlin dan akses melalui delegasi properti ini di seluruh aplikasi Anda. Parameter filename memberi tahu DataStore file mana yang akan digunakan untuk menyimpan data, dan parameter serializer memberi tahu DataStore nama class serialisasi yang ditentukan di langkah 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();

Membaca dari Proto DataStore

Gunakan DataStore.data untuk mengekspos Flow properti yang sesuai dari objek yang disimpan.

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());

Menulis ke Proto DataStore

Proto DataStore menyediakan fungsi updateData() yang mengupdate objek yang disimpan secara transaksional. updateData() memberi Anda status data saat ini sebagai instance jenis data dan mengupdate data secara transaksional dalam operasi baca-tulis-modifikasi atomik.

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()));

Menggunakan DataStore dalam kode sinkron

Salah satu manfaat utama DataStore adalah API asinkron, tetapi tidak selalu memungkinkan untuk mengubah kode di sekitarnya menjadi asinkron. Hal ini mungkin terjadi jika Anda menangani codebase yang ada yang menggunakan I/O disk sinkron atau jika Anda memiliki dependensi yang tidak menyediakan API asinkron.

Coroutine Kotlin menyediakan builder coroutine runBlocking() untuk membantu menjembatani jarak antara kode sinkron dan asinkron. Anda dapat menggunakan runBlocking() untuk membaca data dari DataStore secara sinkron. RxJava menawarkan metode pemblokiran di Flowable. Kode berikut memblokir thread panggilan hingga DataStore menampilkan data:

Kotlin

val exampleData = runBlocking { context.dataStore.data.first() }

Java

Settings settings = dataStore.data().blockingFirst();

Menjalankan operasi I/O sinkron pada UI thread dapat menyebabkan ANR atau jank UI. Anda dapat mencegah masalah ini dengan melakukan pramuat data secara asinkron dari DataStore:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

Java

dataStore.data().first().subscribe();

Dengan demikian, DataStore secara asinkron membaca data dan meng-cache-nya dalam memori. Selanjutnya, pembacaan sinkron menggunakan runBlocking() dapat lebih cepat atau menghindari operasi I/O disk sepenuhnya jika pembacaan awal telah selesai.

Menggunakan DataStore dalam kode multiproses

Anda dapat mengonfigurasi DataStore untuk mengakses data yang sama di berbagai proses, dan konsistensi data terjamin tetap sama seperti berasal dari dalam satu proses. Secara khusus, DataStore menjamin:

  • Operasi baca hanya menampilkan data yang telah disimpan ke disk.
  • Konsistensi operasi baca setelah tulis.
  • Penulisan diserialisasi.
  • Operasi baca tidak pernah diblokir oleh operasi tulis.

Sebaiknya satu aplikasi contoh berisi satu layanan dan satu aktivitas:

  1. Layanan berjalan dalam proses terpisah dan secara berkala memperbarui 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. Aplikasi akan mengumpulkan perubahan tersebut, lalu mengupdate UI-nya

    val settings: Settings by dataStore.data.collectAsState()
    Text(
      text = "Last updated: $${settings.timestamp}",
    )
    

Agar dapat menggunakan DataStore di berbagai proses, Anda harus membuat objek DataStore menggunakan MultiProcessDataStoreFactory.

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

serializer akan memberi tahu DataStore cara membaca dan menulis jenis data. Pastikan Anda menyertakan nilai default untuk serialisasi yang akan digunakan jika belum ada file yang dibuat. Berikut adalah contoh implementasi yang menggunakan 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()
       )
   }
}

Anda dapat menggunakan injeksi dependensi Hilt untuk memastikan instance DataStore Anda bersifat unik untuk setiap proses:

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

Memberikan masukan

Sampaikan masukan dan ide Anda kepada kami melalui resource berikut:

Issue tracker
Laporkan masalah agar kami dapat memperbaiki bug.

Referensi lainnya

Untuk mempelajari Jetpack DataStore lebih lanjut, lihat referensi tambahan berikut:

Contoh

Blog

Codelab