DataStore Android Jetpack'in bir parçasıdır.

Jetpack DataStore, protokol arabellekleri ile anahtar/değer çiftlerini veya yazılan nesneleri depolamanıza olanak tanıyan bir veri depolama çözümüdür. DataStore, verileri eşzamansız, tutarlı ve işlemsel olarak depolamak için Kotlin eş yordamlarını ve Flow kullanır.

Veri depolamak için şu anda SharedPreferences kullanıyorsanız bunun yerine DataStore'a geçiş yapabilirsiniz.

Tercihler DataStore ve Proto DataStore

DataStore iki farklı uygulama sunar: Tercihler DataStore ve Proto DataStore.

  • Preferences DataStore, verileri depolar ve anahtarları kullanarak bunlara erişir. Bu uygulama, önceden tanımlanmış bir şema gerektirmez ve tür güvenliği sağlamaz.
  • Proto DataStore, verileri özel bir veri türünün örnekleri olarak depolar. Bu uygulama, protokol arabellekleri kullanarak bir şema tanımlamanızı gerektirir ancak tür güvenliği sağlar.

DataStore'u doğru kullanma

DataStore'u doğru şekilde kullanmak için aşağıdaki kuralları her zaman unutmayın:

  1. Hiçbir zaman aynı işlemde belirli bir dosya için birden fazla DataStore örneği oluşturmayın. Aksi takdirde tüm DataStore işlevleri bozulabilir. Aynı işlemde belirli bir dosya için etkin olan birden fazla DataStore varsa DataStore verileri okurken veya güncellerken IllegalStateException değerini atar.

  2. DataStore'un genel türü sabit olmalıdır. DataStore'da kullanılan bir türün değiştirilmesi, DataStore'un sağladığı tüm garantileri geçersiz kılar ve ciddi, bulunması zor hatalara yol açar. Değişmezlik garantisi, basit bir API ve etkili serileştirme sağlayan protokol arabellekleri kullanmanız önemle tavsiye edilir.

  3. Aynı dosya için SingleProcessDataStore ve MultiProcessDataStore kullanımlarını hiçbir zaman karıştırmayın. DataStore ürününe birden fazla işlemden erişmeyi planlıyorsanız her zaman MultiProcessDataStore kullanın.

Kurulum

Uygulamanızda Jetpack DataStore kullanmak için, kullanmak istediğiniz uygulamaya bağlı olarak aşağıdakileri Gradle dosyanıza ekleyin:

Tercihler DataStore

Modern

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

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

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

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

Kotlin

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

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

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

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

Proto Veri Deposu

Modern

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

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

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

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

Kotlin

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

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

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

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

Anahtar/değer çiftlerini Tercihler DataStore ile depolayın

Tercihler DataStore uygulaması, basit anahtar/değer çiftlerini diskte tutmak için DataStore ve Preferences sınıflarını kullanır.

Tercihler Veri Deposu Oluşturma

Datastore<Preferences> örneği oluşturmak için preferencesDataStore tarafından oluşturulan mülk yetkisini kullanın. Kodu kotlin dosyanızın en üst düzeyinde bir kez çağırın ve uygulamanızın geri kalanında bu mülk üzerinden erişin. Bu sayede DataStore daha kolay bir şekilde tekil olarak tutulur. RxJava kullanıyorsanız alternatif olarak RxPreferenceDataStoreBuilder'i de kullanabilirsiniz. Zorunlu name parametresi, Tercihler DataStore'un adıdır.

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

Tercihler Veri Deposu'ndan okuyun

Tercihler DataStore önceden tanımlanmış bir şema kullanmadığından, DataStore<Preferences> örneğinde depolamanız gereken her değer için bir anahtar tanımlamak üzere ilgili anahtar türü işlevini kullanmanız gerekir. Örneğin, bir int değeri için anahtar tanımlamak üzere intPreferencesKey() değerini kullanın. Ardından, Flow kullanarak uygun depolanan değeri göstermek için DataStore.data özelliğini kullanın.

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

Tercihler Veri Deposu'na yazma

Tercihler DataStore, verileri bir DataStore içinde işlemsel olarak güncelleyen bir edit() işlevi sağlar. İşlevin transform parametresi, değerleri gereken şekilde güncelleyebileceğiniz bir kod bloğunu kabul eder. Dönüşüm bloğundaki tüm kod tek bir işlem olarak ele alınır.

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.

Yazılan nesneleri Proto DataStore ile depolama

Proto DataStore uygulaması, yazılan nesneleri diskte tutmak için DataStore ve protokol arabelleklerini kullanır.

Şema tanımlama

Proto DataStore, app/src/main/proto/ dizinindeki bir proto dosyasında önceden tanımlanmış bir şema gerektirir. Bu şema, Proto DataStore'unuzda tuttuğunuz nesnelerin türünü tanımlar. Proto şeması tanımlama hakkında daha fazla bilgi edinmek için protobuf dil kılavuzuna bakın.

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

Proto DataStore Oluşturma

Yazılan nesnelerinizi depolamak için Proto DataStore iki adımda oluşturulur:

  1. Serializer<T> yöntemini uygulayan bir sınıf tanımlayın. Burada T, proto dosyasında tanımlanan türdür. Bu serileştirici sınıfı, DataStore'a veri türünüzü nasıl okuyup yazacağını anlatır. Henüz dosya oluşturulmadıysa kullanılacak serileştirici için varsayılan bir değer eklediğinizden emin olun.
  2. DataStore<T> örneği oluşturmak için dataStore tarafından oluşturulan özellik temsilcisini kullanın. Burada T, proto dosyasında tanımlanan türdür. Bunu kotlin dosyanızın en üst düzeyinde bir kez çağırın ve uygulamanızın geri kalanında bu özellik temsilcisi aracılığıyla erişin. filename parametresi, DataStore'a verileri depolamak için hangi dosyanın kullanılacağını bildirir. serializer parametresi ise DataStore'a 1. adımda tanımlanan serileştirici sınıfının adını bildirir.

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'dan oku

Depolanan nesnenizden uygun özelliğin bir Flow değerini göstermek için DataStore.data kullanın.

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'a yazma

Proto DataStore, depolanan bir nesneyi işlemsel olarak güncelleyen bir updateData() işlevi sunar. updateData(), veri türünüzün örneği olarak verilerin mevcut durumunu verir ve atomik okuma-yazma değiştirme işleminde verileri işlemsel olarak günceller.

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'u eşzamanlı kodda kullanma

DataStore'un temel avantajlarından biri eşzamansız API'dir ancak etrafındaki kodu eşzamansız olacak şekilde değiştirmek her zaman mümkün olmayabilir. Eşzamanlı disk G/Ç kullanan mevcut bir kod tabanıyla çalışıyorsanız veya eşzamansız API sağlamayan bir bağımlılığınız varsa bu durum yaşanabilir.

Kotlin eş yordamları, eşzamanlı ve eşzamansız kod arasındaki boşluğu doldurmaya yardımcı olmak için runBlocking() eş yordam oluşturucuyu sunar. DataStore'dan verileri eşzamanlı olarak okumak için runBlocking() kullanabilirsiniz. RxJava, Flowable adresinde engelleme yöntemleri sunar. Aşağıdaki kod, DataStore veri döndürene kadar çağrı yapan iş parçacığını engeller:

Kotlin

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

Java

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

Kullanıcı arayüzü iş parçacığında eşzamanlı G/Ç işlemleri gerçekleştirmek, ANR'lere veya kullanıcı arayüzü duraklamalarına neden olabilir. Verileri DataStore'dan eşzamansız olarak önceden yükleyerek bu sorunları azaltabilirsiniz:

Kotlin

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

Java

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

Bu şekilde DataStore verileri eşzamansız bir şekilde okur ve belleğe kaydeder. runBlocking() kullanılarak daha sonra eşzamanlı okuma işlemleri daha hızlı olabilir ya da ilk okuma tamamlandıysa disk G/Ç işleminin tamamen önüne geçebilir.

DataStore'u çoklu işlem kodunda kullanma

DataStore'u, tek bir işlemdekiyle aynı veri tutarlılığı garantileriyle farklı süreçlerde aynı verilere erişecek şekilde yapılandırabilirsiniz. DataStore özellikle şunları garanti eder:

  • Okumalar yalnızca diskte saklanan verileri döndürür.
  • Yazma sonrası okuma tutarlılığı.
  • Yazma işlemleri serileştirilir.
  • Okuma işlemleri, yazma işlemleri tarafından hiçbir zaman engellenmez.

Hizmeti ve etkinliği olan örnek bir uygulamayı ele alalım:

  1. Hizmet ayrı bir işlemde çalışmaktadır ve DataStore'u düzenli olarak

    <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. Uygulama bu değişiklikleri toplayıp kullanıcı arayüzünü günceller ve

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

DataStore'u farklı işlemlerde kullanabilmek için DataStore nesnesini MultiProcessDataStoreFactory kullanarak oluşturmanız gerekir.

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

serializer, DataStore'a veri türünüzü nasıl okuyup yazacağını söyler. Henüz hiçbir dosya oluşturulmadıysa kullanılacak serileştirici için varsayılan bir değer eklediğinizden emin olun. Aşağıda kotlinx.serialization kullanılarak oluşturulmuş bir örnek uygulama görebilirsiniz:

@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()
       )
   }
}

DataStore örneğinizin işlem başına benzersiz olduğundan emin olmak için Hilt bağımlılık ekleme işlemini kullanabilirsiniz:

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

Geri bildirim gönderme

Aşağıdaki kaynakları kullanarak geri bildiriminizi ve fikirlerinizi bizimle paylaşın:

Sorun izleyici
Hataları düzeltebilmemiz için sorunları bildirin.

Ek kaynaklar

Jetpack DataStore hakkında daha fazla bilgi edinmek için aşağıdaki ek kaynaklara bakın:

Numuneler

Bloglar

Codelab uygulamaları