Od jednog jela do celog restorana


U našim prethodnim lekcijama, kuhinja je postala mašina za obradu pojedinačnih narudžbina. Gost zatraži jednu stvar, a suspend funkcija vrati jedan rezultat.

Ali pravi restoran je mnogo složeniji. To je dinamično okruženje sa više tokova informacija:

  • Švedski sto na koji se neprestano iznose nova jela.
  • Barmen i kuvar koji moraju da sinhronizuju porudžbinu hrane i pića.
  • Tabla sa statusom uživo kako bi gosti mogli da prate svoju porudžbinu.
  • Razglas za objave osoblju.

suspend funkcije nisu dovoljne za ovo. Da bismo upravljali višestrukim vrednostima tokom vremena, potrebna nam je biblioteka za korutine: Kotlin Flow.

Švedski sto: Flow (hladni strim)


Flow je asinhroni strim vrednosti. Zamislite ga kao švedski sto. Kuvar (producer) postavlja jela na liniju jedno po jedno (emit-uje vrednosti), a gost (consumer) uzima svako jelo kako postane dostupno (collect-uje vrednosti).

Ključno je da je standardni Flow hladan. To znači da kuvar ne počinje da kuva dok se gost ne pojavi da preuzme jelo. Ako niko ne preuzima, nikakav posao se ne obavlja. To ga čini izuzetno efikasnim.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun serveDishes(): Flow<String> = flow {
    println("Kuvar: 'Gost je stigao! Počinjem sa kuvanjem.'")
    delay(1000); emit("Testenina")
    delay(1000); emit("Salata")
    delay(1000); emit("Hleb")
}

fun main() = runBlocking {
    println("Gost: 'Želim da jedem sa švedskog stola.'")
    serveDishes().collect { dish ->
        println("Gost: 'Mmm, ovo je dobro: $dish!'")
    }
    println("Gost: 'Sit sam!'")
}

Kuvar počinje da kuva tek kada se pozove .collect, a tok se prirodno završava kada kuvar više nema jela za emit-ovanje.

Hladni-Flow

Orkestriranje obroka: Napredni Flow operatori


Pravom restoranu je potrebno da kombinuje različite tokove. Flow pruža bogat skup operatora za to.

Uparivanje jela i pića pomoću zip-a

Gost naručuje kompletan obrok: jelo i piće. Kuvar priprema hranu, a barmen piće. Moraju se poslužiti zajedno kao par. To je posao zip-a. On kombinuje dva toka tako što uparuje njihove odgovarajuće elemente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val foodFlow = flowOf("Šnicla", "Salata", "Supa")
    val drinkFlow = flowOf("Vino", "Voda", "Sok")

    foodFlow.zip(drinkFlow) { food, drink ->
        "$food sa $drink"
    }.collect { meal ->
        println("Konobar: 'Služim: $meal'")
    }
}

Izlaz:

1
2
3
Konobar: 'Služim: Šnicla sa Vinom'
Konobar: 'Služim: Salata sa Vodom'
Konobar: 'Služim: Supa sa Sokom'

zip čeka dok ne dobije novu stavku iz oba toka pre nego što emituje kombinovani rezultat. Zaustavlja se čim se jedan od tokova završi.

Zip

Tabla sa menijem uživo pomoću combine-a

Zamislite digitalnu tablu sa menijem. Ona treba da prikazuje “Jelo dana”, koje se menja na svakih nekoliko sekundi. Takođe treba da prikazuje “Trenutnu cenu”, koja se zasniva na tržišnim troškovima i ažurira se u drugačijem intervalu.

Operator combine je savršen za ovo. On kombinuje poslednju vrednost iz svakog toka svaki put kada jedan od njih emituje novu vrednost.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    // Jelo dana (menja se na svake 3s)
    val dishFlow = flow {
        delay(1500); emit("Losos")
        delay(1500); emit("Piletina")
    }

    // Cena (ažurira se na svake 2s)
    val priceFlow = flow {
        delay(1000); emit("$25")
        delay(1000); emit("$23")
        delay(1000); emit("$24")
    }

    dishFlow.combine(priceFlow) { dish, price ->
        "Današnja preporuka: $dish za $price"
    }.collect { menu ->
        println("Tabla sa menijem: $menu")
    }
}

Izlaz:

1
2
3
4
5
6
7
8
// ~1.5s: Stiže prvo jelo
Tabla sa menijem: Današnja preporuka: Losos za $25
// ~2s: Cena se ažurira
Tabla sa menijem: Današnja preporuka: Losos za $23
// ~3s: Stiže drugo jelo, cena je i dalje $23
Tabla sa menijem: Današnja preporuka: Piletina za $23
// ~3s: Cena se ponovo ažurira
Tabla sa menijem: Današnja preporuka: Piletina za $24

Ovo je izuzetno korisno za korisnički interfejs (UI) gde je potrebno kombinovati više izvora podataka da bi se prikazao ekran.

Combine

Tabla sa statusom porudžbine: StateFlow (vrući strim)


Naš švedski sto (Flow) je bio hladan. Ali šta je sa podacima koji postoje bez obzira da li ih neko gleda ili ne, kao što je status porudžbine? Za to je potreban vrući strim.

StateFlow je kao velika digitalna tabla sa statusom porudžbine.

  • Vruć je: Uvek je aktivan i uvek ima vrednost (npr. “Porudžbina primljena”).
  • Čuva samo poslednju vrednost. Stari statusi su zauvek nestali.
  • Novi posmatrači odmah dobijaju trenutni status, a zatim i sva buduća ažuriranja.

StateFlow je standardni alat za upravljanje stanjem UI-a u ViewModel-u.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ViewModel
private val _orderStatus = MutableStateFlow("Porudžbina primljena")
val orderStatus: StateFlow<String> = _orderStatus.asStateFlow()

fun updateStatus(newStatus: String) {
    _orderStatus.value = newStatus
}

// UI
viewModel.orderStatus.collect { status ->
    println("Aplikacija gosta: 'Status je: $status'")
}
StateFlow

Razglas: SharedFlow (vrući strim za događaje)


StateFlow je za stanje. Ali šta je sa jednokratnim događajima (events), poput “Plaćanje uspešno” toast poruke ili komande za navigaciju? Ako koristimo StateFlow, rotacija ekrana može dovesti do ponovnog prikazivanja događaja.

Za događaje nam je potreban SharedFlow. Zamislite ga kao razglas u kuhinji.

  • Vruć je: Razglas je uvek uključen.
  • Emituje događaje svim trenutnim slušaocima.
  • Podrazumevano, novi slušaoci ne primaju stare objave.

Ovo je savršeno za prikazivanje snackbar-a ili navigaciju na novi ekran tačno jednom.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ViewModel
private val _announcements = MutableSharedFlow<String>()
val announcements: SharedFlow<String> = _announcements.asSharedFlow()

suspend fun makeAnnouncement(message: String) {
    _announcements.emit(message)
}

// UI
viewModel.announcements.collect { announcement ->
    println("(Konobar je čuo): '$announcement'")
}
SharedFlow

Zaključak

  • Flow: Hladni strim za podatke na zahtev, poput švedskog stola. Koristite ga za pozive repozitorijuma koji preuzimaju podatke iz baze ili sa mreže.
  • Napredni operatori:
    • zip: Uparuje stavke iz više strimova jedan-na-jedan.
    • combine: Kreira novu vrednost od poslednje stavke svakog strima. Ključan za reaktivne UI-je.
  • StateFlow: Vrući strim za predstavljanje stanja UI-a, poput table sa statusom. Koristite ga za čuvanje stanja ekrana u vašem ViewModel-u.
  • SharedFlow: Vrući strim za emitovanje jednokratnih događaja, poput razglasa. Koristite ga za slanje događaja kao što su toast poruke ili komande za navigaciju iz vašeg ViewModel-a.

Šta nas čeka u četvrtom delu?

Naš restoran je sada efikasan, obrađuje složene porudžbine i tokove informacija. Ali do sada smo radili pod idealnim uslovima. Šta se dešava kada nastane večernja gužva i naš sistem se nađe pod stvarnim opterećenjem?

Moramo našu efikasnu kuhinju pretvoriti u zaista otpornu operaciju spremnu za produkciju.

  • Šta se dešava kada kuhinja (proizvođač) proizvodi jela brže nego što konobari (potrošači) mogu da ih posluže? Da li bacamo jela na pod ili imamo strategiju? To je backpressure.
  • Kako da osiguramo da intenzivno seckanje i kuvanje (Dispatchers.IO) nikada ne ometa delikatan posao postavljanja tanjira i serviranja (Dispatchers.Main)? Videćemo operator flowOn.
  • Ako se jedno jelo u neprekidnom toku pripremi pogrešno, kako možemo da obradimo tu grešku pomoću catch operatora, a da ne ugasimo ceo švedski sto?

U sledećem delu, zaronićemo u napredne operatore i koncepte koji čine Flow dovoljno robusnim za svaki scenario koji mu možete postaviti.