Kotlin Coroutines: Android Asenkron Programlama Dünyasına Hızlı Bir Giriş

Kerim Bora
16 min readMar 27, 2023

Asenkron programlama, bir işlemin tamamlanmasını beklemek yerine, işlem sırasında başka işlemlerin yapılmasına izin veren bir programlama paradigmasıdır. Bu işlemler arka planda çalışabilir ve birbirleriyle etkileşim halinde olabilirler.

Asenkron Programlama Zaman Kazanç Grafiği

Coroutine Nedir ?

Coroutine (Türkçe karşılığı ile eş zamanlı iş parçacığı veya eşzamanlı görev), programlamada bir işlem veya iş parçacığının başlatılması, duraklatılması ve devam ettirilmesi için kullanılan bir yapıdır.

Coroutine’ ler, kodunuzda bulunan işlemleri birbirinden bağımsız parçalara ayırarak, bunları eşzamanlı olarak veya arka planda gerçekleştirerek uygulamanızın daha hızlı ve verimli çalışmasını sağlar. Yani, coroutine’ ler sayesinde programlar, işlemler sırasında beklemek zorunda kalmadan birden fazla işlemi aynı anda gerçekleştirebilirler.

Kotlin’ deki coroutine’ ler, Kotlin 1.3 sürümü ile birlikte tanıtılmıştır ve bu sayede Kotlin programlama dilinde asenkron işlemler daha kolay ve okunaklı bir şekilde yazılabilmektedir. Coroutine’ lerin temel fikri, bir işlemi başlatmak, duraklatmak ve devam ettirmek için kullanılan işaretçileri (pointer) yönetmek ve bu sayede eşzamanlı işlemi sağlamaktır.

Coroutine’ ler, iş parçacıklarına (thread) benzer şekilde çalışır ancak daha hafif bir yapıya sahiptirler. Bir coroutine, bir iş parçacığı içinde veya farklı iş parçacıkları arasında çalışabilir. Coroutine’ ler, yüksek kaynak kullanımına sebep olmadan, kodun okunaklılığını ve bakımını kolaylaştırmak için kullanılır.

Coroutine ile Thread Arasındaki Fark Nedir ?

Coroutine ve Thread, asenkron programlama için kullanılan iki farklı yapıdır. İkisi arasındaki en temel fark, coroutine’ lerin daha hafif bir yapıya sahip olması ve daha az kaynak tüketmesidir.

Thread’ ler, işlemci çekirdeklerinin kullanımını kontrol eden bir yapıdır ve birden fazla iş parçacığının (thread) aynı anda çalışmasını sağlar. Thread’ler, bellek yönetimi, senkronizasyon, blokaj ve işlemci kaynaklarının paylaşımı gibi çeşitli zorluklarla karşılaşabilirler. Ayrıca, thread’ lerin yaratılması, başlatılması ve yönetimi gibi işlemler, ek yük ve karmaşıklık oluşturabilir.

Coroutine’ ler ise, thread’ lerden daha hafif bir yapıya sahiptir ve birden fazla işlemi aynı anda gerçekleştirebilirler. Coroutine’lerin çalışması, işlemci çekirdeklerinin kullanımını kontrol etmez ve daha az kaynak tüketir. Coroutine’lerin yaratılması ve yönetimi, thread’lerden daha kolaydır ve kodun okunaklılığı ve bakımı için daha uygun bir yapıya sahiptirler.

Coroutines kullanmanın temel avantajları şunlardır:

  • Hafif oldukları için uygulamanın performansını etkilemezler.
  • İptal işlemleri için özel destekleri vardır.
  • Bellek sızıntılarını (memory leaks) önlemeleri için tasarlanmışlardır.
  • Jetpack kütüphaneleri tarafından coroutine desteği sunulmaktadır.

Kotlin Coroutines Kütüphanesi (kotlinx.coroutines)

Android’de Coroutine’ler, asenkron işlemler için kullanılan ve uygulamanın performansını artırmak için tasarlanmış bir yapıdır. Coroutine’ler, Android uygulamalarında özellikle ağ işlemleri, veritabanı işlemleri ve uzun süren işlemler için kullanılır. Coroutine’ler, Java’nın Executor Framework ve RxJava gibi diğer popüler asenkron işlemler kütüphanelerine göre daha basit bir yapıya sahiptir ve daha az bellek tüketir.

Android Studio 3.0 ve sonrasında, Kotlin dilinin bir parçası olarak, Coroutine’ leri kullanmak için ek bir kütüphane yüklemenize gerek yoktur.

Kotlin Coroutine ile ilgili temel terminolojiler şunlardır:

  • Suspend Functions Asenkron işlemleri yönetmek için kullanılan ve suspend anahtar kelimesiyle işaretlenmiş normal bir Kotlin fonksiyonudur.
  • Coroutine Scope Coroutine’ leri çalıştırmak için gerekli olan kapsam scope nesnesidir. CoroutineScope, coroutine’ leri başlatmak ve durdurmak için gerekli olan işlevleri sağlar.
  • Coroutine Builder CoroutineScope’ un bir parçası olarak çalışan fonksiyonlardır. Coroutine’ lerin oluşturulması ve yönetilmesi için kullanılırlar. Örneğin, launch ve async gibi coroutine builder’ ları, farklı koşullar altında coroutine’ leri başlatmak ve sonlandırmak için kullanılır.
  • Dispatcher Coroutine’ lerin hangi thread veya thread havuzunda çalışacağını belirlemek için kullanılır. Coroutine’ ler, önceden belirlenmiş thread havuzlarına atanarak çalışabilirler.
  • Job Coroutine’ lerin çalışma zamanı işlemlerinin kontrolünü sağlar. Job, bir coroutine’in başlatılması ve sonlandırılması sırasında kullanılır ve coroutine’in durumunu takip etmek için kullanılır.

Suspend Functions Nedir ?

Suspend functions, Kotlin programlama dilindeki özel bir fonksiyon türüdür ve asenkron işlemleri yönetmek için kullanılır. Bu fonksiyonlar, suspend anahtar kelimesiyle işaretlenir ve async/await yapısına benzer şekilde çalışırlar. Suspend fonksiyonları, coroutine’ lerde kullanılmak üzere tasarlanmıştır ve coroutine’ in çalışmasını durdurarak, işlem yapılması gereken sırada başka işlerle ilgilenilebilmesini sağlar.

suspend fun fetchUser() {
// do something
}

Yukarıda verilen görüntüdeki gibi, functionA askıya alınırken, functionB aynı iş parçacığında (thread) yürütmeye devam eder.

Suspend fonksiyonlar, işlemlerin bloklanmadan asenkron olarak yürütülmesini sağlar. Örneğin, bir ağ isteği sonucunu beklemek gerektiğinde, kullanıcı arayüzünün (UI) bloklanmasını engellerler ve işlemin arkada planda sürdürülmesine izin verirler. Ayrıca, bir coroutine içinde çağrılan bir diğer suspend fonksiyon, işlemler arasında geçiş yaparak daha etkili bir şekilde kaynak kullanımı sağlar.

Coroutine Scope Nedir ?

CoroutineScope, bir veya daha fazla coroutine’ i yürütmek için kullanılan bir yapıdır. Bu yapı, coroutine’ lerin çalışma zamanlarını kontrol etmek ve koordine etmek için kullanılır.

Bir CoroutineScope, bir veya daha fazla CoroutineDispatcher’ a sahip olabilir. CoroutineDispatcher, bir coroutine’ in çalıştığı thread’ i belirler. Örneğin, Dispatchers.Main ana UI thread’ de coroutine’ leri çalıştırmak için kullanılırken, Dispatchers.IO girdi/çıktı işlemlerinin gerçekleştirildiği thread havuzunda coroutine’ leri çalıştırmak için kullanılır.

Kotlin’ de coroutine’ leri çalıştırmak için kullanılan CoroutineScope çeşitleri şunlardır:

  • GlobalScope Bu, uygulama düzeyindeki CoroutineScope’ dur ve uygulamanın sonlanmasına kadar varlığını sürdürür. Yani, tüm uygulama boyunca kullanılan coroutine’ ler için kullanılabilir. Bu scope’ un kullanımı, büyük uygulamalarda önerilmez.
GlobalScope.launch {
// do something
}
  • viewModelScope Bu scope, Android Jetpack tarafından sunulan bir özelliktir ve bir ViewModel ile ilişkilendirilebilir. ViewModelScope, ViewModel tarafından oluşturulduğunda oluşur ve ViewModel öldüğünde iptal edilir. ViewModelScope, bir Activity veya Fragment’ın yaşam döngüsüne bağımlı olmayan coroutine’ler oluşturmak için idealdir. Bu Scope’ u çalıştırmak için aşağıdaki bağımlılık (Jetpack Lifecycle) gereklidir.
dependencies {
...
def lifecycle_version = "2.6.1"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
}
class MyViewModel : ViewModel() {
fun doSomeWork() {
viewModelScope.launch {
// do some work
}
}
}
  • lifecycleScope Bu scope, Android Jetpack tarafından sunulan bir başka özelliktir ve bir Activity veya Fragment’ ın yaşam döngüsüne bağlı olan coroutine’ ler oluşturmak için kullanılır. Bu scope, yaşam döngüsüne bağımlı olan işlemleri gerçekleştirmek için kullanılır ve Activity veya Fragment’ ın ömrü boyunca bu işlemleri yürütmeyi sağlar. Bu Scope’ u çalıştırmak için viewModelScope’ da oluduğu gibi Jetpack Lifecycle bağımlılığına ihtiyaç duyulur.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
// do some work
}
}
}
  • CoroutineScope Bu scope, coroutine’ leri yürütmek için oluşturulur ve yalnızca bir veya birkaç coroutine’ i yürütmek için kullanılması önerilir. Bu scope’ un, lifecycle-aware olması ve manuel olarak iptal edilmesi gerekmektedir. CoroutineScope, ViewModelScope veya LifeCycleScope gibi özel kullanım senaryoları yerine, genel amaçlı kullanım için tasarlanmıştır.
val myScope = CoroutineScope(Dispatchers.IO)
myScope.launch {
// do some work
}
myScope.cancel() // used to cancel coroutines

Dispathcer Nedir

Kotlin Coroutine’ lerinde Dispatcher, coroutine' lerin hangi thread veya thread havuzunda çalışacağını belirlemek için kullanılan bir araçtır.

Dispatcher, coroutine'lerin çalıştırılacağı hedef thread veya thread havuzunu belirlemeye yardımcı olur ve bu nedenle, uygulamaların performansını artırmak ve daha etkili bir şekilde çalışmasını sağlamak için önemlidir.

Kotlin’de kullanılabilen bazı dispatcher’ lar şunlardır:

  • Dispatchers.Main UI işlemleri için kullanılır. Bu, coroutine’i ana iş parçacığında başlatır. Çoğunlukla, coroutine içinde UI işlemleri gerçekleştirmemiz gerektiğinde kullanılır, çünkü UI sadece ana iş parçacığından (aynı zamanda UI iş parçacığı olarak da adlandırılır) değiştirilebilir.
  • Dispatchers.Default Bu, coroutine’i Varsayılan İş Parçacığında başlatır. Karmaşık ve uzun süren hesaplamalar yapacağımız zaman seçmemiz gereken iş parçacığıdır. Bu hesaplamalar ana iş parçacığını engelleyerek UI’nin donmasına neden olabilirler. Örneğin, 10.000 hesaplama yapmamız gerektiğini varsayalım ve tüm bu hesaplamaları ana iş parçacığı (UI iş parçacığı) üzerinde yaptığımızda, sonucu beklememiz gerektiği sürece ana iş parçacığı bloke olacak ve UI’miz donacak, bu da kullanıcı deneyimini kötüleştirecektir. Bu durumda Varsayılan İş Parçacığı kullanmamız gerekiyor. GlobalScope’da coroutine başlatıldığında kullanılan varsayılan görev dağıtıcısı Dispatchers.Default tarafından temsil edilir ve paylaşılan bir arka plan iş parçacığı havuzu kullanır. Bu nedenle launch(Dispatchers.Default) { … } ifadesi, GlobalScope.launch { … } ile aynı görev dağıtıcısını kullanır.
  • Dispatchers.IO Bu, coroutine’ i IO iş parçacığında başlatır. Ağ işlemleri, veri tabanından okuma veya yazma, dosyalara okuma veya yazma gibi tüm veri işlemlerinin gerçekleştirilmesi için kullanılır. Örneğin, veri tabanından veri almak bir IO işlemidir ve IO iş parçacığı üzerinde yapılır.
  • Dispatchers.Unconfined Coroutine’ in devam ettiği thread’ i değiştirmek yerine, coroutine’ in tamamlanmasından sonra farklı bir thread’de yeniden başlatılması gereken işlemler için kullanılır.

Aşağıdaki gibi örnek bir kullanımı vardır

launch(Dispatchers.IO) {
// Dispathcer IO' da uzun süren işlemler yapılır
val result = apiCall()
withContext(Dispatchers.Main) {
// sonuçla ilgili işlemler UI thread'inde yapılır
updateUI(result)
}
}

Coroutine Builder

Coroutine Builder, coroutine’ lerin oluşturulması ve yönetilmesi için kullanılan yapıdır. Coroutine Builder’ lar, coroutine’ lerin nasıl oluşturulacağına ve hangi scope altında çalışacağına karar verirler.

Kotlin’ de, coroutine’ leri oluşturmak için çeşitli coroutine builder’ lar vardır. Bu builder’lar, coroutine’ lerin özelliklerine ve kullanım senaryolarına göre farklılık gösterir. Örneğin, launch() coroutine builder’ı, coroutine’ leri arka planda çalıştırmak için kullanılırken, async() coroutine builder’ı, bir değer döndürmek için kullanılır.

Kotlin’ de yaygın olarak kullanılan coroutine builder’ları şunlardır:

  • launch() coroutine’leri arka planda çalıştırmak için kullanılır. Değer döndürmez.
  • async() coroutine’leri arka planda çalıştırmak için kullanılır ve sonuç döndürür.
  • runBlocking() coroutine’leri senkronize bir şekilde çalıştırmak için kullanılır.
  • withContext() coroutine’leri belirli bir Dispatcher altında çalıştırmak için kullanılır.
  • flow() asenkron veri akışları oluşturmak için kullanılır.

``launch()’’ Builder

launch() Coroutine Builder, bir coroutine’i arka planda çalıştırmak için kullanılan en temel builder’lardan biridir. Bu builder ile coroutine’ lerin çalışması asenkron bir şekilde gerçekleştirilir ve değer döndürmez.

launch() builder’ ı özellikle ayrı bir Thread kullanmadan, bir işlemi arka planda yapmak istediğimiz durumlarda kullanırız. Örneğin, bir butona tıklandığında arka planda bir network işlemi yapmak istediğimizde launch() builder’ ını kullanabiliriz. Bu sayede UI Thread’ i engellemeden, arka planda işlem yaparak uygulamanın performansını artırabiliriz.

Aşağıdaki örnek senaryoda, launch() builder’ını kullanarak bir network işlemini arka planda gerçekleştiriyoruz:

//UI Thread'i engellemeden arka planda bir network işlemi yapmak için
//launch() builder'ını kullanabiliriz.

button.setOnClickListener {
//UI Thread'i engellemeden arka planda network işlemi yapmak için coroutineScope kullanıyoruz.
CoroutineScope(Dispatchers.Main).launch {
//Network işlemleri yapılırken UI Thread'i engellenmesin diye Dispatchers.IO kullanıyoruz.
val result = withContext(Dispatchers.IO) {
performNetworkRequest()
}

//Elde edilen sonuçla birlikte UI Thread'inde işlem yapmak için burada kullanabiliriz.
updateUI(result)
}
}

private suspend fun performNetworkRequest(): String {
//Network işlemleri yapılır
}

private fun updateUI(result: String) {
//UI güncellemeleri yapılır
}

``async()`` Builder

async() coroutine builder, bir asenkron görevi başlatır ve sonucu döndürür. Bu builder, launch() builder’ ının aksine sonucu döndürür, bu nedenle Deferred türünde bir nesne döndürür.

async() builder, özellikle bir işlem sonucunu döndürmek istediğimiz durumlarda kullanışlıdır. Ayrıca, bir dizi asenkron işlemi başlatmak ve sonuçlarını toplamak için de kullanılabilir.

Örnek senaryo: Bir kullanıcının bir hesap bakiyesi var ve bir alışveriş yaptığında hesap bakiyesinin azalması ve alışverişin başarılı bir şekilde tamamlanması gerekiyor. Bu işlemi asenkron olarak yapabiliriz, böylece kullanıcının diğer işlemleri yapmasına izin verebiliriz. Ayrıca, işlem tamamlandığında kullanıcının yeni bakiyesini göstermek istiyoruz.

suspend fun makePurchase(amount: Double): Double = coroutineScope {
val initialBalance = getBalance() // hesap bakiyesini al
val deferredPurchase = async { performPurchase(amount) } // alışverişi yap
val newBalance = initialBalance - deferredPurchase.await() // yeni bakiyeyi hesapla
updateBalance(newBalance) // bakiyeyi güncelle
return@coroutineScope newBalance // yeni bakiyeyi döndür
}

Bu örnekte, makePurchase(Double) adlı bir suspend fonksiyonu kullanarak bir alışveriş işlemi gerçekleştiriyoruz. performPurchase(Double) işlevi, alışverişin gerçekleştirildiği ve sonucun döndüğü bir başka asenkron işlevdir. async builder, bu işlevi arka planda başlatır ve sonucunu Deferred nesnesi olarak döndürür. await yöntemi, alışverişin sonucunu bekler ve ardından yeni hesap bakiyesini hesaplamak için kullanılır. Son olarak, updateBalance(Double) işlevi hesap bakiyesini günceller ve son hesap bakiyesi döndürülür.

``runBlocking()`` Builder

runBlocking() coroutine builder, senkronize bir şekilde coroutine’ leri çalıştırmak için kullanılır. Bu, bir coroutine scope içinde kullanılabilir ve bir işlem tamamlanana kadar çalışması gereken senkron işlemler için kullanılır. Bu builder’ ın kullanımı genellikle test işlevlerinde, ana fonksiyonlarda veya bir uygulama başlatılırken kullanılır.

Örneğin, bir API’ den veri çekmek ve bu verilerle bir işlem yapmak istediğimizi düşünelim. Bu işlem sırasında verileri çekmek için ayrı bir coroutine kullanabiliriz ve bu coroutine’ in tamamlanması beklenmeden uygulama işlemlerine devam edebiliriz. Ancak sonuçları kullanmadan önce tüm coroutine’ lerin tamamlanmış olmasını isteyebiliriz. Bu durumda runBlocking() kullanarak ana thread’ in diğer işlemlerini bloke edebiliriz ve tüm coroutine’ ler tamamlanmadan devam etmemiş oluruz.

fun main() = runBlocking {
println("Main coroutine starts")

val result1 = async { fetchData("https://jsonplaceholder.typicode.com/todos/1") }
val result2 = async { fetchData("https://jsonplaceholder.typicode.com/todos/2") }

println("Result 1: ${result1.await()}")
println("Result 2: ${result2.await()}")

println("Main coroutine ends")
}

suspend fun fetchData(url: String): String {
delay(1000) // simulate network delay
return url
}
Main coroutine starts
Result 1: https://jsonplaceholder.typicode.com/todos/1
Result 2: https://jsonplaceholder.typicode.com/todos/2
Main coroutine ends

Bu örnekte, fetchData(String) fonksiyonu bir URL’ ye bir GET isteği gönderir ve sonucunu bir string olarak döndürür. async builder’ i kullanarak, iki farklı URL için ayrı coroutine’ ler oluşturuyoruz ve her biri fetchData(String) fonksiyonunu çağırarak veri çekiyor. await fonksiyonu, coroutine’ in tamamlanmasını bekler ve sonucu alır.

runBlocking builder’ i, tüm coroutine’ ler tamamlanana kadar ana thread’ i bloke eder ve sonuçlarını yazdırır.

``withContext()`` Builder

withContext() coroutine builder, coroutine’ lerin belirli bir Dispatcher altında çalıştırılmasını sağlar. Bu builder, farklı Dispatchers’ların kullanıldığı ve coroutine’lerin paralel çalıştığı durumlarda çok yararlıdır.

Örneğin, aşağıdaki senaryoda bir API’ den veri çekme işlemi gerçekleştirilecek ve bu verilerin kullanıldığı bir işlem yapılacaktır. Veri çekme işlemi IO Dispatcher’ da gerçekleştirilecek ve daha sonra ana thread üzerinde işlem yapılacaktır.

fun loadData() = runBlocking {
val data = withContext(Dispatchers.IO) {
fetchDataFromApi()
}
processData(data)
}

suspend fun fetchDataFromApi(): String {
delay(3000) // simulate network delay
return "API data"
}

fun processData(data: String) {
println("Received data: $data")
}

Yukarıdaki kod örneğinde, withContext() builder, Dispatchers.IO altında çalışan fetchDataFromApi() fonksiyonunu çağırır ve verileri data adlı değişkene atar. Daha sonra, bu veriler processData(String) fonksiyonu tarafından işlenir.

Job Nedir ?

Kotlin Coroutine’ lerinde çalışan bir iş parçacığı (task) birimi olan Job, bir coroutine'in bir kopyasını temsil eder. Bir coroutine oluşturulduğunda, onu başlatan builder işlemi sonucunda bir Job nesnesi oluşur.

Bu Job nesnesi, başlatılan coroutine'i takip etmek için kullanılır ve onu durdurmak veya iptal etmek gibi kontrol işlemleri yapmak için kullanılır. Job nesnesi, bir coroutine'i temsil ettiği için, onun durumu ve ilerlemesi hakkında bilgi verir. Örneğin, bir coroutine ne zaman başlatıldı, durumunda bir değişiklik oldu mu veya ne zaman tamamlandı gibi bilgileri sağlar.

Ayrıca, bir coroutine’in Job nesnesi, farklı coroutine'ler arasında işbirliği yaparak paralel işlem yapılmasına da olanak tanır. Bir coroutine, başka bir coroutine'in tamamlanmasını bekleyebilir veya birden fazla coroutine'in tamamlanmasını bekleyen bir grup coroutine'in çalıştırılmasını yönetebilir.

Bir Job nesnesi oluşturmak için, launch() veya async() gibi bir coroutine builder kullanarak bir coroutine başlatılır ve bu builder'lar işlevlerinden biri Job nesnesi döndürür. Bu nesne daha sonra, coroutine'in durumunu izlemek, iptal etmek veya hata yönetmek için kullanılabilir.

Örnek olarak, aşağıdaki kodda bir Job nesnesi oluşturulur ve coroutine'in durumu takip edilir:

fun main() = runBlocking {
val job: Job = launch {
// Coroutine işlemleri burada gerçekleştirilir
delay(1000L)
println("Coroutine işlemi tamamlandı")
}

println("Job durumu: ${job.isActive}")

job.join() // Coroutine işlemi tamamlanana kadar bekler

println("Job durumu: ${job.isActive}")
}
Job durumu: true
Coroutine işlemi tamamlandı
Job durumu: false

Kotlin Coroutines: Flow

Flow, asenkron veri akışı sağlayan bir Kotlin kütüphanesi öğesidir. Flow, RxJava ve LiveData gibi kütüphanelerle benzer bir işlevsellik sunar. Ancak, Flow’ un belirgin bir farkı, akışın, akışın kaynağından tüketiciye kadar asenkron olarak işlendiği ve işlemin duraklatılabileceği noktalarda duraklatılabildiği coroutine’ lerle çalışmasıdır.

Flow(Akış) Diagramı
  • Producer akışa eklenen verileri üretir. Coroutine’ ler sayesinde, akışlar aynı zamanda veri üretmeyi asenkron olarak da gerçekleştirebilir.
  • Intermediaries (İsteğe Bağlı) Aracılar, akışa gönderilen her değeri veya akışın kendisini değiştirebilir.
  • Consumer akıştan gelen değerleri tüketir.

Flow, birden fazla değer yayabileceğiniz bir kanal gibi düşünülebilir. Bu sayede, bir akışın herhangi bir noktasında işlem yapılabilir ve akışın devam etmesine izin verilir.

Flow, asenkron veri akışları oluşturmak için kullanılır ve genellikle veri akışlarının işlenmesi gerektiği durumlarda kullanılır. Örneğin, bir web soketi veya bir API’ den gelen verilerin işlenmesinde kullanılabilir. Flow, Reactive Programming ile benzerlik gösterir ve birçok operatörle birlikte çalışabilir.

Flow, birkaç farklı durumda kullanılabilir:

  1. Asenkron veri akışlarının işlenmesi Örneğin, bir web soketi veya bir API’ den gelen verilerin işlenmesinde kullanılabilir. Flow, verilerin arka planda işlenmesine izin verir ve UI thread’ ini bloke etmeden verileri akış halinde işleyebilir.
  2. Veri akışlarını dönüştürme ve işleme Flow, map(), filter(), flatMap() vb. gibi birçok operatörle birlikte kullanılabilir. Bu operatörler, verilerin dönüştürülmesine ve işlenmesine olanak tanır.
  3. Veri akışlarını birleştirme Birden fazla Flow, combine(), zip() vb. gibi operatörlerle birleştirilebilir.

Flow Oluşturma ve İşleme

Adım adım bir Flow veri akışı oluşturmayı ve bu akışın içindeki verilerin nasıl işleneceğini inceleyelim.

fun foo(): Flow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i)
}
}

Bu fonksiyon, Flow<Int> türünde bir akış üretir. Bu akış, flow{...} builder' ı kullanılarak oluşturulur. flow{...} builder' ı bir coroutine builder' ıdır ve suspendable fonksiyonlar içerebilir. Akış elemanları, emit() fonksiyonu kullanılarak yayınlanır. Bu örnekte, 1' den 3' e kadar olan üç sayı akışa eklenir ve her sayı akışa eklendikten sonra 100 ms gecikme eklenir.

fun main() = runBlocking<Unit> {
foo().collect { value ->
println(value)
}
}

foo fonksiyonu çağrılır ve collect fonksiyonu kullanılarak akış elemanları alınır. collect fonksiyonu, bir suspend fonksiyonudur ve bir lambda fonksiyonu olarak parametre alır. Bu lambda fonksiyonu, her elemanı aldığında çalışır ve her elemanın değerini ekrana yazdırır.

Bu örnek kod, Flow'un nasıl kullanılabileceğini gösterir. Flow, asenkron ve arka planda çalışan veri akışları oluşturmak için kullanılır. Flow, veri işleme işlevselliğini kolaylaştırır ve daha az bellek kullanarak daha yüksek performans sağlar.

Örnek olarak, bir API’den gelen verileri bir Flow halinde işlemek aşağıdaki gibi yapılabilir:

fun fetchUserData(): Flow<User> = flow {
// API'den kullanıcı verilerini al
val userData = api.getUserData()

// Kullanıcı verilerini birer birer akışa ekle
userData.forEach { emit(it) }
}

// Flow'u işleme örneği
fetchUserData().filter { it.age > 18 }.map { it.name }.collect {
Log.d(TAG, "User name: $it")
}

Bu örnekte, fetchUserData() adlı bir Flow tanımlanır ve API'den kullanıcı verilerini alır. Veriler, bir Flow olarak emit edilir. Daha sonra, filter() ve map() operatörleri kullanılarak veriler işlenir ve son olarak collect() ile işlenen verilerin son halleri loglanır.

StateFlow ve SharedFlow

StateFlowve SharedFlow, Kotlin Flow API’ sinin bir parçasıdır ve Android uygulama geliştirme sürecinde veri akışını yönetmek için kullanılır.

StateFlow

StateFlow, Kotlin Coroutines Flow kütüphanesi tarafından sağlanan bir tür akışdır. StateFlow, akışın mevcut durumunu tek bir değer olarak saklar ve akışın tüketildiği sürece bu değer güncellenebilir.

StateFlow, durum yönetimi gerektiren birçok durumda kullanılabilir. Örneğin, bir ekranın belirli bir durumunu takip etmek için kullanılabilir. Örneğin, bir uygulamanın ana ekranındaki bir sayaç, bir kullanıcının oturum açma durumu veya bir veritabanından alınan sonuç gibi durumlar, StateFlow kullanılarak yönetilebilir.

StateFlow’ un özellikleri şunlardır:

  • Immutable StateFlow, tek bir durumu temsil eder ve bu durum sadece bir kez atanabilir. Yani, StateFlow üzerinde yapılan herhangi bir değişiklik, yeni bir StateFlow nesnesi oluşturulmasını gerektirir.
  • Thread-SafeStateFlow, birden çok coroutine tarafından aynı anda kullanılabilir ve güvenli bir şekilde güncellenebilir.
  • Observe EdilebilirStateFlow, Flow kütüphanesi tarafından sağlanan diğer akış türleri gibi tüketilebilir ve gözlemlenebilir.

Örneğin, bir sayacın durumunu StateFlow kullanarak yönetmek için aşağıdaki gibi bir kod yazılabilir:

class CounterViewModel : ViewModel() {
private val _counter = MutableStateFlow(0)
val counter: StateFlow<Int> = _counter

fun incrementCounter() {
_counter.value++
}
}

Bu örnekte, _counter adında bir MutableStateFlow nesnesi tanımlanır ve başlangıç değeri olarak 0 atanır. _counter nesnesi, val counter: StateFlow<Int> = _counter satırıyla counter adlı bir StateFlow nesnesine atanır ve dışarıya açılır. Bu sayede, counter nesnesi uygulamanın diğer bileşenleri tarafından okunabilir ve takip edilebilir. incrementCounter() fonksiyonu ise _counter nesnesinin değerini artırarak sayaç mantığı oluşturur.

Android uygulamaları, farklı yaşam döngüsü aşamalarında bulunur. Örneğin, bir Activity oluşturulur, başlatılır, duraklatılır, devam ettirilir, durdurulur ve sonunda yok edilir. Aynı şekilde, bir Fragment de oluşturulur, eklenir, duraklatılır, devam ettirilir, kaldırılır ve sonunda yok edilir. Bu farklı yaşam döngüsü aşamaları, uygulama bileşenlerinin ne zaman ve nasıl oluşturulduklarını, kullanıcı ara yüzünün nasıl göründüğünü ve diğer davranışları belirler.

LiveData, yaşam döngüsü bilinci olan bir veri sahibidir. Bu nedenle, uygulama bileşenleri (Activity, Fragment vb.) yaşam döngüsüne bağlı olarak otomatik olarak güncellenir. Örneğin, bir Activity, onStart() veya onResume() yöntemleri çağrıldığında, LiveData’ daki son güncellemeyi otomatik olarak alır ve görüntülemek için kullanabilir. Benzer şekilde, bir Fragment, onCreateView() yöntemi çağrıldığında, son güncelleme otomatik olarak alınır ve görüntülenir.

StateFlow ise, yaşam döngüsü bilinci olmadan çalışır ve akışa abone olan tüm gözlemciler için güncelleme yapar. Bu nedenle, StateFlow ile kullanılan uygulama bileşenleri, StateFlow’ un son değerini almak için manuel olarak abone olmalıdır.

Özetle, LiveData, yaşam döngüsü bilinci olan bir veri sahibidir ve uygulama bileşenleri yaşam döngüsüne bağlı olarak otomatik olarak güncellenirken, StateFlow yaşam döngüsü bilinci olmadan çalışır ve akışa abone olan tüm gözlemciler için güncelleme yapar. Bu fark, uygulama bileşenlerinin güncellemeleri almak için manuel olarak abone olması gerektiği anlamına gelir.

SharedFlow

Kotlin Shared Flow, Kotlin Coroutines tarafından sunulan bir akış (stream) işlemidir. Shared Flow, birden fazla gözlemci (observer) tarafından aynı anda kullanılabilir ve gözlemcilerin yeni verileri eş zamanlı olarak almasına izin verir.

Shared Flow, veri akışını çok sayıda gözlemci arasında paylaşmak istediğinizde özellikle kullanışlıdır. Gözlemciler arasında veri paylaşımı, tek bir veri kaynağından verileri alarak gereksiz işlemci kaynaklarından tasarruf etmenizi sağlar.

StateFlow, SharedFlow’ un özelleştirilmiş bir halidir.

Özetle, StateFlow, bir akışın tek bir kaynaktan değişen bir değişken veriyi temsil etmek için kullanılırken, SharedFlow, bir akışın birden çok kaynaktan güncellenebileceği durumlar için kullanılır ve veri depolama için kullanılabilir.

SharedFlow, diğer Flow türlerinde olduğu gibi, verilerin asenkron olarak aktığı bir akıştır. Ancak, SharedFlow, yayınlanan verilerin bellekte tutulması ve sonradan aboneler tarafından tüketilmesi özelliği ile diğer Flow türlerinden farklılaşır. Yani, bir veri yayınlandığında, bu veri daha sonra bir abone tarafından tüketilene kadar bellekte saklanır.

Bir örnek senaryo düşünelim: bir haber uygulaması. Bu uygulamada, yeni bir haber geldiğinde, bu haberi tüm abonelere göstermek istiyoruz. Ancak, haberlerin bir son kullanma tarihi var ve bu tarihten sonra artık görüntülenemezler. Ayrıca, uygulamamızda daha önce kaydedilen haberlerin de gösterilmesini istiyoruz.

Bu senaryo için, SharedFlow kullanarak bir haber akışı oluşturabiliriz. Bu akış, yayınlanan tüm haberleri içerecek ve daha sonra aboneler tarafından tüketilecektir. Ayrıca, bu haberlerin son kullanma tarihlerini de içereceğiz, böylece daha sonra görüntülenemezler.

Flow ve LiveData Lifecycle İlişkisi

Flow, StateFlow ve SharedFlow, LiveData gibi lifecycle-aware akışlar değillerdir. LiveData, Android Architecture Components kütüphanesinin bir parçasıdır ve özel olarak Android bileşenlerinin yaşam döngüsü ile uyumlu olarak tasarlanmıştır. LiveData, veri değişikliklerini otomatik olarak tespit eder ve verileri yalnızca aktif olan yaşam döngüsü sahibi bileşenlere yayınlar. Bu nedenle, LiveData, özellikle bir Activity veya Fragment gibi yaşam döngüsü sahibi bileşenlerde kullanıldığında, kullanıcı arayüzü ile ilgili verilerin güncellenmesinde çok yararlıdır.

Öte yandan, Flow, StateFlow ve SharedFlow, herhangi bir yaşam döngüsü bilgisi olmadan da kullanılabilirler ve genel olarak Kotlin Coroutine Streams API’nin bir parçasıdır. Bu nedenle, bu akışlar, LiveData gibi Android bileşenlerinin yaşam döngüsü ile otomatik olarak uyumlu değillerdir ve uygulama geliştiricileri, bu akışları kullanırken dikkatli olmalı ve uygun yaşam döngüsü yönetimini sağlamalıdır.

class MyViewModel : ViewModel() {

private val _myFlow = flow {
// Asynchronous operations
emit("Data 1")
delay(1000)
emit("Data 2")
delay(1000)
emit("Data 3")
}

val myLiveData: LiveData<String> = _myFlow.asLiveData()
}

Bu örnekte, _myFlow adlı bir Flow tanımlanır ve birkaç veri akışı gönderir. _myFlow daha sonra asLiveData() işlevi aracılığıyla LiveData'ya dönüştürülür ve myLiveData adlı bir değişkene atanır. Bu LiveData değişkeni, View'a bağlanabilir ve LifecycleOwner ile uyumlu hale getirilebilir. Örneğin, bir Activity sınıfındaki onCreate() yönteminde şu şekilde bağlanabilir:

class MyActivity : AppCompatActivity() {

private lateinit var myViewModel: MyViewModel
private lateinit var myTextView: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)

myViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
myTextView = findViewById(R.id.my_text_view)

myViewModel.myLiveData.observe(this, Observer { data ->
myTextView.text = data
})
}
}

.asLiveData() uzantısı ile Flow’ u LiveData’ a dönüştürebiliriz.

--

--

No responses yet