Android — Kotlin Flows

Buğra Yetkin
5 min readFeb 28, 2023

Selamlar, bugün bahsetmek istediğim konu data stream için kullandığımız kotlin flows. Flow ile data akışı kontrolü, operatörler yardımıyla başka bir flow ile birleştirme (zip, combine), sadece son gönderilen datayı handle etme (collectLatest), hataları handle etme (catch) gibi birçok işlem yapabiliyoruz ayrıca işlemleri farklı scope’larda da yapabiliyoruz. Buradan scope’ları inceleyebilirsiniz.

Ufak bir girizgahtan sonra örnekler ile anlatmaya çalışacağım. Keyifli okumalar.

Android is the hardest but we love it :)

What is Reactive Programming ?

everything is a stream and it’s observable

Observer pattern ile data değişimini-akışını iletmek için kullanılıyor. Kendi ui logic’lerimize göre yeni bir data akışı başlatıp, gelen data durumuna göre de ui bu stat’e göre update edilebilir. Örneğin; Kullanıcı bir button’a tıkladığında başlayan data isteklerinin ardından gelecek olan response’a göre ui yeni halini alacaktır. Bu akışlar birbirinden bağımsız (asynchronous) olarak çalışabilir en önemlisi akışlar combine, map, filter gibi oldukça kolaylık ve fayda sağlayan operatörler ile destekleniyor böylelikle akışımıza yön verebiliyoruz. Flows da bu kısımda bize yardımcı olacak aslında.

Kotlin Flows

Flow ile elimizdeki datayı ui katmanına taşıma ile başlayalım öncelikle viewModel’da getUsername fonksiyonu ve flowBuilder ile flow oluşturuyorum → flow { } içerisine data göndermek istediğimde emit( ) ‘i kullanıyorum. Bu kod bloğu “ 1 ” saniye gecikmeli olarak string değeri emit edecektir. Normalde thread ile işlem yapsaydık Thread.sleep yaptığımızda main thread’i blockladığı için bizim için kullanışlı olmuyordu fakat delay kullandığımızda aslında arka planda bir timer oluşturuluyor ve sadece bizim scope block’lanmış oluyor timer’daki süre dolunca main thread’i blocklamadan kod bloğumuz çalışmaya devam ediyor.

fun getUsername() = flow {
delay(ONE_SECOND)
emit("Android Developer")
}

Artık bu flow’u ui katmanımda collect ederek datayı alabilirim. Data işlemleri ayrı bir scope’ta yaptıktan sonra ui’da bir işlem yapacaksanız bunun için main thread’te olmanız (ui işlemlerini main thread dışında yapamıyoruz) gerekiyor yoksa app crash olacaktır bunun için;

  • Activity için → lifecycleScope (default olarak dispatcher main kullanılıyor)
  • Fragment için → viewLifecycleOwner fragment view lifecycle ile birlikte çalışacaktır.
  • Bunlar dışında coroutineScope’a Dispatchers.Main verebilirsiniz.

! Not : Flows lifecycle aware değildir bu yazımı da inceleyebilirsiniz.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}

private val viewModel: MainViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)

lifecycleScope.launch {
viewModel.getUsername().collect { username ->
binding.txtUsername.text = username
}
}
}
}

Flow’da emit, collect ve bazı operatörler suspend function olduğu için coroutine scope içerisinde kullanılması gerekiyor. Bu scope’lar yardımıyla işlemlerinizi sequential veya concurrency olarak yapabilirsiniz. Kullandığınız flow’da işlemleri farklı bir scope’ta yapmak isterseniz flow.flowOn(Dispatchers.IO) ile dispatchers verebilirsiniz (coroutines dispatchers).

// ViewModel
fun getUserResponseFlowBuilder() = flow {
delay(ONE_SECOND)
emit("User 1")
}.flowOn(Dispatchers.IO)

// Activity
CoroutineScope(Dispatchers.Main).launch {
viewModel.getUserResponseFlowBuilder().collect {
binding.txtUsername.text = it
}
}

Flow’u direkt layout’unuzda da kullanabilirsiniz collect etmeden datanız direkt güncellenecektir. DataBinding’in flow ve liveData desteği mevcut aşağıdaki kod bloğunda ufak bir örneği var. Binding’e lifecycleOwner veriyoruz ve ilgili flow’u xml içerisinde kullanıyoruz.

android:text=”@{viewModel.userResponseStateFlow}”

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
binding.lifecycleOwner = this
}

<TextView
android:id="@+id/txtUsername"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.userResponseStateFlow}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

Ufak bir örnek daha gösterip sonrasında flow types’a geçeceğiz. Flow operators için buraya bakabilirsiniz. Aşağıdaki kod bloğunda normal listeleri de flow’a çevirebildiğimizi görüyoruz. Sonrasında iki listeyi birleştiriyoruz ve birleştirme esnasında iki listedeki o anki değeri alabiliyoruz numberOne + numberTwo işlemi ile gelen 2 değeri topluyoruz. Reduce operatörü bize eski yapılan işlem ile yeni gelen değeri de kullanarak tekrar bir işlem yapmamızı sağlıyor açıklamak gerekirse ( log’lar üzerinden anlaşılacağı üzere accumulator değeri init’te 0–2 (flow listesinden ) ‘den gelen 2 ile başlıyor ardından toplamı işlemi newValue ile yapıldığında accumulator içerisinde saklanıyor sonrasında toplama işlemi yapıldığında accumulator değeri toplamı tuttuğu için son adımda bize result olarak dönüyor)

lifecycleScope.launch {
val numbersOne = listOf(0, 1, 2, 3).asFlow()
val numbersTwo = listOf(2, 5, 6, 7).asFlow()

val result = numbersOne.zip(numbersTwo) { numberOne, numberTwo ->
numberOne + numberTwo
}.reduce { accumulator, newValue ->
Log.e("Accumulator --> ", "$accumulator")
Log.e("NewValue --> ", "$newValue")
accumulator + newValue
}

Log.e("Result --> ", "$result")
}

// OUTPUT
// Accumulator --> 2
// NewValue --> 6
// Accumulator --> 8
// NewValue --> 8
// Accumulator --> 16
// NewValue --> 10
// Result --> 26

Flow Types

  • FlowBuilder → configuration change olduğu zaman son değeri koruyamıyor ve data kaybı oluyor.
  • StateFlow → İnit value zorunlu ,configuration change olduğu zaman son değeri koruyabiliyor fakat bu değer ile sürekli olarak işlem yapabilir örneğin; kullanıcının trigger ettiği bir data ise trigger olmadan change işlemi gerçekleşince sürekli gösterilmeye devam eder. Ayrıca emit edilen değer öncekinden farklı değilse tekrar emit edilmeyecektir.
  • SharedFlow → İnit value zorunlu değil ,kullanıcının trigger ettiği ui işlemleri için kullanabiliriz. Shared flow’u collect ederken öncesinde data emit edilmiş olabilir bu durumda önceki dataları belirleyeceğimiz sayıda replay ile alabiliriz. Bunu flowBuilder’da .shareIn ile yapabiliriz. örn; MutableSharedFlow<String>(replay = 5)

🧐 Hot & Cold Flow

🔥 Hot Flow Multicast (çoklu yayın) ve dağıtılacak datanın collect (observer) dışarısında üretilmesi yani observe işlemi başlamadan öncede datanın var olması — Akışı başlatmazlar ona bağlanırlar, bağlandıkları zamana bağlı olarak data kaybı yaşayabilirler. (data akışı başladıktan sonra receive işlemine başlanırsa kayıp olabilir) BroadcastChannel kullanılabilir.(Diğer channel’lara da bakabilirsiniz)

val channelUserSurname = BroadcastChannel<String>(10)
fun getUserSurname() = viewModelScope.launch {
channelUserSurname.trySend("Hello !")
delay(ONE_SECOND)
channelUserSurname.trySend("Android")
delay(ONE_SECOND)
channelUserSurname.trySend("Developer")
}
// 1. Launch
launch {
Log.e("Collection First", "starting...")
viewModel.channelUserSurname.consumeEach { username ->
Log.e("Collection First", "$username collected")
binding.txtUsername.text = username
}
}
// 2. Launch
launch {
delay(ONE_SECOND)
Log.e("Collection Second", "starting...")
viewModel.channelUserSurname.consumeEach { username ->
Log.e("Collection Second", "$username collected")
binding.txtUsername.text = username
}
}

// OUTPUT
// Collection First starting...
// Collection First Hello ! collected
// Collection Second starting...
// Collection First Android collected
// Collection Second Android collected
// Collection First Developer collected
// Collection Second Developer collected

⚠️ Yukarıdaki işlemde tek fark 2. launch 1 saniye delay ile başlıyor ve sadece ilk datayı kaçırıyor sonrasında 2 kısımda da aynı dataları handle edebiliyoruz. Bu işlemi direkt flow ile yapsaydık 2 taraftada datalar yeniden oluşturulacaktı yani multi cast olmayacaktı.

🥶 Cold FlowUnicast (tekli yayın) ve dağıtılacak dataların collect (observer) işlemi başlayınca üretilmeye başlaması — Akışın aldığı her yeni abonelik için, kodunun yeni bir yürütmesi tetiklenir ve öncekilerden bağımsız olur. Dolayısıyla, aynı akışın birden fazla örneğine sahip olabileceğimizi ve bunların tamamen bağımsız olduğunu söyleyebiliriz. (Yukarıdaki örneği flow ile yapsaydık işlem her collect işlemi yapıldığında yeniden başlayacaktı.)

Twitter veya LinkedIn üzerinden feedback veya sorularınız için iletişime geçebilirsiniz elimden geldiğince yardımcı olmaya çalışırım. Herkese iyi çalışmalar umarım bu zor günleri hep birlikte atlatabiliriz.👋

--

--