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

Kotlin Multiplatform ile deneme
Kotlin Multiplatform, veri katmanının diğer platformlarla paylaşılmasına olanak tanır. KMP'de DataStore'u nasıl ayarlayacağınızı ve kullanacağınızı öğrenin.

Jetpack DataStore, anahtar-değer çiftlerini veya protocol buffers ile yazılmış 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 coroutine'lerini ve Flow'u kullanır.

Verileri depolamak için şu anda SharedPreferences'i kullanıyorsanız bunun yerine DataStore'a geçiş yapmayı düşünebilirsiniz.

Preferences DataStore ve Proto DataStore

DataStore, iki farklı uygulama sunar: Preferences DataStore ve Proto DataStore.

  • Preferences DataStore, verileri anahtarlar kullanarak depolar ve verilere 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 göz önünde bulundurun:

  1. Aynı işlemde belirli bir dosya için hiçbir zaman birden fazla DataStore örneği oluşturmayın. Bu işlem, tüm DataStore işlevlerinin bozulmasına neden olabilir. Aynı işlemde belirli bir dosya için birden fazla DataStore etkinse DataStore, verileri okurken veya güncellerken IllegalStateException hatası verir.

  2. DataStore<T> öğesinin 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 yakalanması zor, ciddi hatalara yol açabilir. Değişmezlik garantileri, basit bir API ve verimli serileştirme sağlayan protokol arabelleklerini kullanmanız kesinlikle önerilir.

  3. Aynı dosya için SingleProcessDataStore ve MultiProcessDataStore kullanımını asla karıştırmayın. DataStore öğesine birden fazla işlemden erişmeyi planlıyorsanız her zaman MultiProcessDataStore kullanın.

Kurulum

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

Tercihler Veri Deposu

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

Anahtar/değer çiftlerini Preferences DataStore ile saklama

Preferences DataStore uygulaması, basit anahtar/değer çiftlerini diske kalıcı olarak kaydetmek için DataStore ve Preferences sınıflarını kullanır.

Preferences DataStore oluşturma

DataStore<Preferences> örneği oluşturmak için preferencesDataStore tarafından oluşturulan mülk temsilcisini kullanın. Kotlin dosyanızın en üst düzeyinde bir kez çağırın ve uygulamanızın geri kalanında bu özellik üzerinden erişin. Bu sayede DataStore öğenizi tekil olarak tutmanız kolaylaşır. Alternatif olarak, RxJava kullanıyorsanız RxPreferenceDataStoreBuilder kullanın. Zorunlu name parametresi, Preferences 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 DataStore'undan okuma

Preferences 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() kullanın. Ardından, Flow kullanarak uygun saklanan 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));

Preferences DataStore'a yazma

Tercihler Veri Deposu, edit() işlemlere dayalı olarak DataStore içindeki verileri güncelleyen bir işlev sağlar. İşlevin transform parametresi, değerleri gerektiği gibi güncelleyebileceğiniz bir kod bloğunu kabul eder. Dönüşüm bloğundaki tüm kodlar tek bir işlem olarak değerlendirilir.

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 ile türü belirtilmiş nesneleri depolama

Proto DataStore uygulaması, türü belirlenmiş nesneleri diske kalıcı olarak kaydetmek 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 kalıcı hale getirdiğiniz 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.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Proto Veri Deposu oluşturma

Türlenmiş nesnelerinizi depolamak için Proto DataStore oluşturma işlemi iki adımdan oluşur:

  1. Serializer<T> 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ı söyler. Henüz dosya oluşturulmamışsa kullanılacak seri hale getirici için varsayılan bir değer eklediğinizden emin olun.
  2. dataStore tarafından oluşturulan mülk temsilcisini kullanarak DataStore<T> örneği oluşturun. Burada T, proto dosyasında tanımlanan türdür. Bunu Kotlin dosyanızın ü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ı, serializer parametresi ise 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 okuma

Depolanmış nesnenizdeki uygun mülkün 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 sağlar. updateData(), veri türünüzün bir örneği olarak verilerin mevcut durumunu verir ve verileri atomik bir okuma-yazma-değiştirme işleminde 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 çevreleyen kodunuzu her zaman eşzamansız olacak şekilde değiştirmek mümkün olmayabilir. Bu durum, eşzamanlı disk G/Ç'si kullanan mevcut bir kod tabanıyla çalışıyorsanız veya eşzamanlı olmayan bir API sağlamayan bir bağımlılığınız varsa geçerli olabilir.

Kotlin eş yordamları, senkron ve asenkron kod arasındaki boşluğu kapatmaya yardımcı olmak için runBlocking() eş yordam oluşturucuyu sağlar. DataStore'dan verileri eşzamanlı olarak okumak için runBlocking() kullanabilirsiniz. RxJava, Flowable üzerinde engelleme yöntemleri sunar. Aşağıdaki kod, DataStore verileri döndürene kadar çağırma iş parçacığını engeller:

Kotlin

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

Java

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

Kullanıcı arayüzü ileti dizisinde senkron G/Ç işlemleri gerçekleştirmek ANR'lere veya kullanıcı arayüzünde takılmaya neden olabilir. Bu sorunları, DataStore'daki verileri eşzamansız olarak önceden yükleyerek 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 olarak okur ve bellekte önbelleğe alır. Daha sonra runBlocking() kullanılarak yapılan senkron okumalar, ilk okuma tamamlandıysa daha hızlı olabilir veya disk G/Ç işlemi tamamen atlanabilir.

DataStore'u çok süreçli kodda kullanma

DataStore'u, tek bir işlemden erişimle aynı veri tutarlılığı garantileriyle farklı işlemler arasında aynı verilere erişecek şekilde yapılandırabilirsiniz. Özellikle DataStore şunları garanti eder:

  • Okuma işlemleri yalnızca diske kalıcı olarak kaydedilmiş verileri döndürür.
  • Yazma işleminden sonra okuma tutarlılığı.
  • Yazma işlemleri sıralı olarak gerçekleştirilir.
  • Okuma işlemleri hiçbir zaman yazma işlemleri tarafından engellenmez.

Bir hizmet ve etkinliğe sahip örnek bir uygulamayı ele alalım:

  1. Hizmet ayrı bir süreçte çalışır ve DataStore'u düzenli olarak günceller.

    <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 toplarken kullanıcı arayüzünü de günceller.

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

DataStore'u farklı işlemler arasında kullanabilmek için MultiProcessDataStoreFactory kullanarak DataStore nesnesini 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 dosya oluşturulmamışsa kullanılacak seri hale getirici için varsayılan bir değer eklediğinizden emin olun. Aşağıda, kotlinx.serialization kullanılarak yapılan bir örnek uygulama verilmiştir:

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

DataStore örneğinizin süreç başına benzersiz olduğundan emin olmak için Hilt bağımlılık eklemeyi kullanabilirsiniz:

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

Dosya bozulmasını ele alma

DataStore'un diskteki kalıcı dosyasının bozulabileceği nadir durumlar vardır. DataStore, varsayılan olarak bozulmadan otomatik olarak kurtarılmaz ve DataStore'dan okuma girişimleri sistemin CorruptionException oluşturmasına neden olur.

DataStore, bu tür bir senaryoda sorunsuz bir şekilde kurtulmanıza ve istisna oluşturmaktan kaçınmanıza yardımcı olabilecek bir bozulma işleyici API'si sunar. Yapılandırıldığında, bozulma işleyici, bozuk dosyayı önceden tanımlanmış bir varsayılan değer içeren yeni bir dosyayla değiştirir.

Bu işleyiciyi ayarlamak için by dataStore() içinde veya DataStoreFactory fabrika yönteminde DataStore örneğini oluştururken corruptionHandler sağlayın:

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

Geri bildirim gönder

Geri bildirimlerinizi ve fikirlerinizi aşağıdaki kaynaklar aracılığıyla bizimle paylaşabilirsiniz:

Issue Tracker
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:

Örnekler

Bloglar

Codelab uygulamaları