Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin Coroutines. Flow is coming

Kotlin Coroutines. Flow is coming

As a part of kotlinx.coroutines 1.2 Flow was introduced as a cold asynchronous data stream. How does it work? What we can do with it? Is it that we waiting so long for replacing Rx? The talk has answers to them.

Kirill Rozov

August 03, 2019
Tweet

More Decks by Kirill Rozov

Other Decks in Programming

Transcript

  1. 1. Channels 2. Flows 3. Custom operator 4. Concurrent Flow

    5. Flow constraints 6. Adapter 7. Reactive Stream 8. Backpressure 9. Third paNy integrations Agenda
  2. Async in Kotlin // Single value suspend fun foo(p: Params):

    Value = withContext(Dispatchers.Default) { bar(p) } // Collection of values suspend fun foo(p: Params): List<Value> = buildList { while (hasMore) add(nextValue) } // Stream of values fun foo(p: Params): Sequence<Value> = sequence { while (hasMore) yield(nextValue) }
  3. Async in Kotlin // Single value suspend fun foo(p: Params):

    Value = withContext(Dispatchers.Default) { bar(p) } // Collection of values suspend fun foo(p: Params): List<Value> = buildList { while (hasMore) add(nextValue) } // Stream of values fun foo(p: Params): Sequence<Value> = sequence { while (hasMore) yield(nextValue) } Blocking
  4. Channels fun CoroutineScope.fooProducer(p: Params) : ReceiveChannel<Value> { return produce {

    while (hasMore) send(nextValue) } } val values: ReceiveChannel<Value> = fooProducer(p) if (someCondition) { return anotherResult // Oops! Leaked channel } // ... do further work with values ...
  5. • Works with data sources that are intrinsically hot
 Data

    sources that exist without application’s requests for them • incoming network connections • event streams • Synchronization primitives • For transfer data between coroutines • Synchronize access to the same data from multiply coroutines Channels
  6. Flow is a cold asynchronous data stream that executed sequentially

    (in the same coroutine) emits values and completes normally or with an exception kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
  7. GlobalScope.launch { flowOf(1, 2, 3) .map { it * it

    } .collect { print(it) } } Flow
  8. Flow interface Flow<out T> { suspend fun collect(collector: FlowCollector<T>) }

    interface FlowCollector<in T> { suspend fun emit(value: T) }
  9. Builders // From fixed set of values val flowA =

    flowOf(1, 2, 3) // From function type () -> T val flowB = { repeat(3) { it + 1 } }.asFlow() // Sequential call to the emit() function val flowC = flow { emit(1) emit(2) emit(3) }
  10. fun <T> Iterator<T>.asFlow(): Flow<T> fun <T> Iterable<T>.asFlow(): Flow<T> fun <T>

    emptyFlow(): Flow<T> fun <T> Array<T>.asFlow(): Flow<T> fun IntRange.asFlow(): Flow<Int> fun <T> Sequence<T>.asFlow(): Flow<T> fun <T> (() -> T).asFlow(): Flow<T> fun <T> flow( block: suspend FlowCollector<T>.() -> Unit ): Flow<T> Builders
  11. • map / mapNotNull • switchMap • combineLatest • debounce

    / sample • delayEach / delayFlow • Zlter / ZlterNot / ZlterIsInstance / ZlterNotNull • zip • catch • onEach / onCompletion • \atMapConcat / \atMapMerge • \a]enConcat / \a]enMerge Intermediate
 Operators * Full list of available operators can be found in official documentation kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
  12. • collect • single / Zrst • toList / toSet

    / toCollection • count • fold / reduce • launchIn / produceIn / broadcastIn Terminal Operators
  13. Built-in
 Operators interface Flow<out T> { suspend fun collect(collector: FlowCollector<T>)

    } // Extension-oriented design approach fun <T, R> Flow<T>.map( transform: suspend (value: T) -> R ): Flow<R>
  14. Custom
 Operator fun <T : Number> Flow<T>.sqrts(): Flow<Double> { return

    flow { // this: FlowCollector<Double> collect { number -> val value = sqrt(number.toDouble()) emit(value) } } }
  15. Custom
 Operator fun <T : Number> Flow<T>.sqrts(): Flow<Double> { return

    flow { // this: FlowCollector<Double> collect { number -> val value = sqrt(number.toDouble()) emit(value) } } } GlobalScope.launch { flowOf(1, 2, 3) .map { sqrt(it.toDouble()) } .sqrts() .collect() }
  16. Custom
 Operator fun <T : Number> Flow<T>.sqrts(): Flow<Double> { return

    flow { // this: FlowCollector<Double> collect { number -> val value = sqrt(number.toDouble()) emit(value) } } } GlobalScope.launch { flowOf(1, 2, 3) .map { sqrt(it.toDouble()) } .sqrts() .collect() }
  17. Concurrent Flow fun <T> Flow<T>.buffer( capacity: Int = Channel.UNLIMITED ):

    Flow<T> = flow { val channel = coroutineScope.produce(capacity) { collect { send(it) } } channel.consumeEach { emit(it) } }
  18. Concurrent Flow fun <T> Flow<T>.buffer(capacity: Int): Flow<T> fun <T> Flow<T>.produceIn(

    scope: CoroutineScope ): ReceiveChannel<T> fun <T> Flow<T>.broadcastIn( scope: CoroutineScope, start: CoroutineStart = CoroutineStart.LAZY ): BroadcastChannel<T> fun <T> channelFlow( block: suspend ProducerScope<T>.() -> Unit ): Flow<T>
  19. • Context preservation
 Flow encapsulates its own execution context and

    never propagates or leaks it downstream Flow Constraints
  20. Context Preservation flow { withContext(Dispatchers.Default) { emit(longComputation()) } } java.lang.IllegalStateException:

    Flow invariant is violated: flow was collected in [Context A info], but emission happened in [Context B info]. Please refer to 'flow' documentation or use 'flowOn' instead
  21. Context Preservation flow { GlobalScope.launch(Dispatchers.IO) // is prohibited withContext(Dispatchers.Default) //

    is prohibited emit(1) // OK coroutineScope {
 emit(2) // OK -- still the same coroutine
 } }
  22. • Context preservation
 Flow encapsulates its own execution context and

    never propagates or leaks it downstream • Exception transparency
 Flow implementations never catch or handle exceptions that occur in downstream <ows Flow Constraints
  23. flow<Any> { try { emitValue() } catch (e: Exception) {

    println("Error handled $e") } } .map { process1() } .collect { process2() } Exception Transparency
  24. flow<Any> { emitValue() } .map { process1() } // catches

    exceptions in emitValue() and process1() .catch { e -> println("Error handled $e") } // throws exceptions from process2() .collect { process2() } Exception Transparency
  25. interface Stream<D> { fun subscribe(callback: Callback<D>) fun unsubscribe(callback: Callback<D>) }

    interface Callback<D> { fun onNext(item: D) fun onError(error: Throwable) fun onCompleted() } Old ways adapter
  26. val stream = Stream<Value>() callbackFlow<Value> { // this: ProducerScope<Value> //

    ProducerScope<Value> extends SendChannel<Value> val callback = object : Callback<Value> { override fun onNext(item: Value) { offer(item) } override fun onError(error: Throwable) { close(error) } override fun onCompleted() { close() } } stream.subscribe(callback) awaitClose { stream.unsubscribe(callback) } } Old ways adapter
  27. RxJava Operator fun <T> Observable<T>.filter( predicate: (T) -> Boolean ):

    Observable<T> = ObservableFilter(this, predicate)
  28. RxJava Operator class ObservableFilter<T>( private val source: ObservableSource<T>, private val

    predicate: (T) -> Boolean ) : Observable<T>(), HasUpstreamObservableSource<T> { override fun source(): ObservableSource<T> = source override fun subscribeActual(observer: Observer<in T>) { source.subscribe(FilterObserver(observer, predicate)) } internal class FilterObserver<T>( actual: Observer<in T>, private val filter: (T) -> Boolean ) : BasicFuseableObserver<T, T>(actual) { override fun onNext(t: T) { if (sourceMode == QueueFuseable.NONE) { try { if (filter(t)) downstream.onNext(t) } catch (e: Throwable) { fail(e) } } else { downstream.onNext(null) } } override fun requestFusion(mode: Int) = transitiveBoundaryFusion(mode) override fun poll(): T? { while (true) { val v = qd.poll() if (v == null || filter(v)) return v } } } } fun <T> Observable<T>.filter( predicate: (T) -> Boolean ): Observable<T> = ObservableFilter(this, predicate)
  29. Flow Operator fun <T> Flow<T>.filter( predicate: suspend (T) -> Boolean

    ): Flow<T> { return flow { collect { if (predicate(it)) emit(it) } } }
  30. Migration from RxJava RxJava 2 Coroutine Single<T> Deferred<T> !❄ Maybe<T>

    Deferred<T?> !❄ Completable Job !❄ Observable<T> Channel<T> ! Flow<T> ❄ Flowable<T>
  31. fun <T> Observable<T>.toFlow(): Flow<T> fun <T> Single<T>.toFlow(): Flow<T> fun <T>

    Maybe<T>.toFlow(): Flow<T> fun <T> Flowable<T>.toFlow(): Flow<T> RxJava2 to Flow Adapters
  32. fun <T> Observable<T>.toFlow(): Flow<T> fun <T> Single<T>.toFlow(): Flow<T> fun <T>

    Maybe<T>.toFlow(): Flow<T> fun <T> Flowable<T>.toFlow(): Flow<T> NO FROM THE BOX :) WRITE YOU OWN RxJava2 to Flow Adapters
  33. @Dao interface SampleDao { } // Request data and observe

    changes with RxJava @Query("SELECT * FROM ENTITIES") fun queryEntities(): Observable<List<Entity>> Room // Blocking request data @Query("SELECT * FROM ENTITIES") fun queryEntities(): List<Entity>
  34. @Dao interface SampleDao { } // Request data and observe

    changes with RxJava @Query("SELECT * FROM ENTITIES") fun queryEntities(): Observable<List<Entity>> Room // Blocking request data @Query("SELECT * FROM ENTITIES") fun queryEntities(): List<Entity> // Request data using Coroutines @Query("SELECT * FROM ENTITIES") suspend fun queryEntities(): List<Entity>
  35. @Dao interface SampleDao { } // Request data and observe

    changes with RxJava @Query("SELECT * FROM ENTITIES") fun queryEntities(): Observable<List<Entity>> Room // Blocking request data @Query("SELECT * FROM ENTITIES") fun queryEntities(): List<Entity> // Request data using Coroutines @Query("SELECT * FROM ENTITIES") suspend fun queryEntities(): List<Entity> // Request data and observe changes using Coroutines @Query("SELECT * FROM ENTITIES") fun queryEntities(): Flow<List<Entity>>
  36. interface GitHubService { } RetroZt @GET("users/{user}/repos") suspend fun listRepos( @Path("user")

    user: String ): List<Repo> @GET("users/{user}/repos") fun listRepos( @Path("user") user: String ): Call<List<Repo>>
  37. interface GitHubService { } RetroZt @GET("users/{user}/repos") suspend fun listRepos( @Path("user")

    user: String ): List<Repo> @GET("users/{user}/repos") fun listRepos( @Path("user") user: String ): Flow<List<Repo>> @GET("users/{user}/repos") fun listRepos( @Path("user") user: String ): Call<List<Repo>>
  38. fun <T> Call<T>.asFlow(): Flow<T> = callbackFlow { enqueue(object : Callback<T>

    { override fun onFailure(call: Call<T>, error: Throwable) { close(error) } override fun onResponse(call: Call<T>, response: Response<T>) { if (response.isSuccessful) { offer(response.body()!!) close() } else { close(RetrofitError(response.errorBody())) } } }) awaitClose { if (!isCanceled) { cancel() } } } class RetrofitError(val errorBody: ResponseBody?) : Throwable() RetroZt
  39. • Flow will be primary way to handle async stream

    of data • Channels will be used: • for hot streams • “under-the-hood” in some Flow operators • as synchronization primitive between Coroutines What will be the next?