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 ayarlama ve kullanma

Jetpack DataStore, anahtar-değer çiftlerini veya protocol buffer 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 SharedPreferences kullanıyorsanız bunun yerine DataStore'a geçiş yapmayı düşünebilirsiniz.

DataStore API

DataStore arayüzü aşağıdaki API'yi sağlar:

  1. DataStore'dan veri okumak için kullanılabilecek bir akış

    val data: Flow<T>
    
  2. DataStore'daki verileri güncelleme işlevi

    suspend updateData(transform: suspend (t) -> T)
    

DataStore Yapılandırmaları

Anahtarları kullanarak verileri depolamak ve verilere erişmek istiyorsanız önceden tanımlanmış bir şema gerektirmeyen ve tip güvenliği sağlamayan Preferences DataStore uygulamasını kullanın. SharedPreferences benzeri bir API'ye sahiptir ancak paylaşılan tercihlerle ilişkili dezavantajları yoktur.

DataStore, özel sınıfları kalıcı hale getirmenize olanak tanır. Bunu yapmak için verilerle ilgili bir şema tanımlamanız ve verileri kalıcı bir biçime dönüştürmek için Serializer sağlamanız gerekir. Protocol Buffers, JSON veya başka bir serileştirme stratejisini kullanmayı seçebilirsiniz.

Kurulum

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

Preferences DataStore

Gradle dosyanızın bağımlılıklar bölümüne aşağıdaki satırları ekleyin:

Groovy

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

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-preferences-core:1.2.1"
    }
    

Kotlin

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

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-preferences-core:1.2.1")
    }
    

İsteğe bağlı RxJava desteği eklemek için aşağıdaki bağımlılıkları ekleyin:

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.2.1"

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

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.2.1")

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

DataStore

Gradle dosyanızın bağımlılıklar bölümüne aşağıdaki satırları ekleyin:

Groovy

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation "androidx.datastore:datastore:1.2.1"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-core:1.2.1"
    }
    

Kotlin

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation("androidx.datastore:datastore:1.2.1")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-core:1.2.1")
    }
    

RxJava desteği için aşağıdaki isteğe bağlı bağımlılıkları ekleyin:

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.2.1"

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

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.2.1")

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

İçeriği serileştirmek için Protocol Buffers veya JSON serileştirme bağımlılıklarını ekleyin.

JSON serileştirme

JSON serileştirmeyi kullanmak için Gradle dosyanıza aşağıdakileri ekleyin:

Groovy

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
    }
    

Kotlin

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
    }
    

Protobuf serileştirme

Protobuf serileştirmeyi kullanmak için Gradle dosyanıza aşağıdakileri ekleyin:

Groovy

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1"

    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

Kotlin

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1")
    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

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ığı tutarlılığı geçersiz kılar ve yakalanması zor, ciddi hatalara yol açabilir. Değişmezlik, net bir API ve verimli serileştirme sağlamaya yardımcı olan protokol arabelleklerini kullanmanızı öneririz.

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

Veri Tanımı

Preferences DataStore

Verileri diske kalıcı olarak kaydetmek için kullanılacak bir anahtar tanımlayın.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

JSON veri deposu için, kalıcı olmasını istediğiniz verilere @Serialization ek açıklaması ekleyin.

@Serializable
data class Settings(
    val exampleCounter: Int
)

Serializer<T> uygulayan bir sınıf tanımlayın. Burada T, daha önce ek açıklamayı eklediğiniz sınıfın türüdür. Henüz dosya oluşturulmamışsa kullanılacak seri hale getirici için varsayılan bir değer eklediğinizden emin olun.

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings(exampleCounter = 0)

    override suspend fun readFrom(input: InputStream): Settings =
        try {
            Json.decodeFromString<Settings>(
                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(t)
                .encodeToByteArray()
        )
    }
}

Proto DataStore

Proto DataStore uygulaması, türü belirlenmiş nesneleri diske kalıcı olarak kaydetmek için DataStore ve protokol arabelleklerini kullanır.

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.

src/main/proto klasörüne settings.proto adlı bir dosya ekleyin:

syntax = "proto3";

option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 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'un veri türünüzü nasıl okuyup yazdığını tanımlar. Henüz oluşturulmuş bir dosya yoksa kullanılacak seri hale getirici için varsayılan bir değer eklediğinizden emin olun.

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) {
        return t.writeTo(output)
    }
}

Veri deposu oluşturma

Verileri kalıcı hale getirmek için kullanılan dosyanın adını belirtmeniz gerekir.

Preferences DataStore

Preferences DataStore uygulaması, anahtar/değer çiftlerini diske kalıcı olarak kaydetmek için DataStore ve Preferences sınıflarını kullanır. DataStore<Preferences> öğesinin bir örneğini oluşturmak için preferencesDataStore tarafından oluşturulan özellik temsilcisini kullanın. Kotlin dosyanızın en üst düzeyinde bir kez çağırın. Uygulamanızın geri kalanında bu mülk aracılığıyla DataStore'a erişin. Bu sayede DataStore'unuzu tekil olarak tutmanız kolaylaşır. Zorunlu name parametresi, Preferences DataStore'un adıdır.

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

JSON DataStore

dataStore tarafından oluşturulan mülk temsilcisini kullanarak DataStore<T> örneği oluşturun. Burada T, serileştirilebilir veri sınıfıdır. 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ı, serializer parametresi ise daha önce tanımlanan serileştirici sınıfının adını bildirir.

val Context.dataStore: DataStore<Settings> by dataStore(
    fileName = "settings.json",
    serializer = SettingsSerializer,
)

Proto DataStore

dataStore tarafından oluşturulan mülk temsilcisini kullanarak DataStore<T> öğesinin bir örneğini oluşturun. Burada T, proto dosyasında tanımlanan türdür. 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 daha önce tanımlanan serileştirici sınıfının adını bildirir.

val Context.dataStore: DataStore<Settings> by dataStore(
    fileName = "settings.pb",
    serializer = SettingsSerializer,
)

Veri deposundan okuma

Verileri kalıcı hale getirmek için kullanılan dosyanın adını belirtmeniz gerekir.

Preferences DataStore

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, bir akış kullanarak uygun depolanmış değeri göstermek için DataStore.data özelliğini kullanın.

fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences ->
    preferences[EXAMPLE_COUNTER] ?: 0
}

JSON DataStore

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

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Proto DataStore

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

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Bir ViewModel tarafından oluşturulan Flow öğesini composable'da kullanmak için collectAsStateWithLifecycle öğesini kullanın. Bu, DataStore akışını güvenli bir şekilde yeniden oluşturmayı tetikleyen Compose durumuna dönüştürür.

@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
  val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
  Text(text = "Example counter: ${counter}")
}

collectAsStateWithLifecycle hakkında daha fazla bilgi için Durum ve Jetpack Compose başlıklı makaleyi inceleyin.

DataStore'a yazma

DataStore, depolanan bir nesneyi işlemsel olarak güncelleyen bir updateData işlevi sağlar. updateData, verilerin mevcut durumunu veri türünüzün bir örneği olarak verir ve verileri atomik bir okuma-yazma-değiştirme işleminde işlemsel olarak günceller. updateData bloğundaki tüm kodlar tek bir işlem olarak değerlendirilir.

Preferences DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData {
        it.toMutablePreferences().also { preferences ->
            preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
        }
    }
}

JSON DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy(exampleCounter = settings.exampleCounter + 1)
    }
}

Proto DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy { exampleCounter = exampleCounter + 1 }
    }
}

Compose uygulamasında DataStore'u kullanma

Compose uygulamasında DataStore'u kullanmak için Android uygulama mimarisi yönergelerini uygulayarak DataStore işlemlerini veri katmanınızda (ör. bir depoda) tutun ve verileri ViewModel aracılığıyla kullanıcı arayüzünüze sunun.

Composable işlevlerinizde doğrudan DataStore'dan okuma veya DataStore'a yazma işlemlerinden kaçının.

  1. DataStore'u bir ViewModel aracılığıyla kullanıma sunun. Deponuzu (DataStore'u sarmalayan) ViewModel içine aktarın ve Flow öğesini StateFlow öğesine dönüştürün. Böylece kullanıcı arayüzü, aşağıdaki snippet'te gösterildiği gibi kolayca gözlemleyebilir:

    class SettingsViewModel(
        private val userPreferencesRepository: UserPreferencesRepository
    ) : ViewModel() {
    
        // Expose the DataStore flow as a StateFlow for Compose
        val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = UserSettings.getDefaultInstance()
            )
    
        fun updateCounter(newValue: Int) {
            viewModelScope.launch {
                userPreferencesRepository.updateCounter(newValue)
            }
        }
    }
    
  2. Composable'ınızdan gözlem yapma ve yazma Kullanıcı arayüzünüzdeki StateFlow öğesini güvenli bir şekilde gözlemlemek için collectAsStateWithLifecycle öğesini kullanın ve aşağıdaki snippet'te gösterildiği gibi yazma işlemlerini gerçekleştirmek için ViewModel işlevlerini çağırın:

    @Composable
    fun SettingsScreen(
        viewModel: SettingsViewModel = viewModel()
    ) {
        // Safely collect the state
        val settings by viewModel.userSettings.collectAsStateWithLifecycle()
    
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "Current counter: ${settings.counter}")
    
            Spacer(modifier = Modifier.height(8.dp))
    
            Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) {
                Text("Increment Counter")
            }
        }
    }
    

DataStore'u çok işlemli kodda kullanma

DataStore'u, tek bir işlemdekiyle aynı veri tutarlılığı özellikleriyle farklı işlemler arasında aynı verilere erişecek şekilde yapılandırabilirsiniz. Özellikle DataStore aşağıdaki özellikleri sağlar:

  • 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.

Hizmetin ayrı bir işlemde çalıştığı ve DataStore'u düzenli olarak güncellediği bir hizmet ve etkinlik içeren örnek bir uygulamayı ele alalım.

Bu örnekte JSON DataStore kullanılmaktadır ancak Preferences veya Proto DataStore da kullanabilirsiniz.

@Serializable
data class Time(
    val lastUpdateMillis: Long
)

Serileştirici, DataStore'ya veri türünüzü nasıl okuyup yazacağını söyler. Henüz oluşturulmuş bir dosya yoksa 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:

object TimeSerializer : Serializer<Time> {

    override val defaultValue: Time = Time(lastUpdateMillis = 0L)

    override suspend fun readFrom(input: InputStream): Time =
        try {
            Json.decodeFromString<Time>(
                input.readBytes().decodeToString()
            )
        } catch (serialization: SerializationException) {
            throw CorruptionException("Unable to read Time", serialization)
        }

    override suspend fun writeTo(t: Time, output: OutputStream) {
        output.write(
            Json.encodeToString(t)
                .encodeToByteArray()
        )
    }
}

DataStore öğesini farklı işlemler genelinde kullanabilmek için hem uygulama hem de hizmet kodu için MultiProcessDataStoreFactory kullanarak DataStore nesnesini oluşturmanız gerekir:

val dataStore = MultiProcessDataStoreFactory.create(
    serializer = TimeSerializer,
    produceFile = {
        File("${context.filesDir.path}/time.pb")
    },
    corruptionHandler = null
)

AndroidManifiest.xml öğenize aşağıdakileri ekleyin:

<service
    android:name=".TimestampUpdateService"
    android:process=":my_process_id" />

Hizmet, updateLastUpdateTime işlevini düzenli olarak çağırır. Bu işlev, updateData kullanarak veri deposuna yazar.

suspend fun updateLastUpdateTime() {
    dataStore.updateData { time ->
        time.copy(lastUpdateMillis = System.currentTimeMillis())
    }
}

Uygulama, veri akışını kullanarak hizmet tarafından yazılan değeri okur:

fun timeFlow(): Flow<Long> = dataStore.data.map { time ->
    time.lastUpdateMillis
}

Artık tüm bu işlevleri MultiProcessDataStore adlı bir sınıfta bir araya getirebilir ve bir uygulamada kullanabiliriz.

Hizmet kodu:

class TimestampUpdateService : Service() {
    val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }


    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serviceScope.launch {
            while (true) {
                multiProcessDataStore.updateLastUpdateTime()
                delay(1000)
            }
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }
}

Uygulama kodu:

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }

// Display time written by other process.
val lastUpdateTime by multiProcessDataStore.timeFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Last updated: $lastUpdateTime",
    fontSize = 25.sp
)

DisposableEffect(context) {
    val serviceIntent = Intent(context, TimestampUpdateService::class.java)
    context.startService(serviceIntent)
    onDispose {
        context.stopService(serviceIntent)
    }
}

DataStore örneğinizin işlem başına benzersiz olması 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 kurtulmaz 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 dosya ile 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.filesDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

Geri bildirim gönderme

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

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:

Örnekler

Bloglar

Codelab uygulamaları