Slide 1

Slide 1 text

Do you even (Kotlin) Flow? The new API for Reactive Programming Ricardo Costeira @rcosteira79 Photo by Riccardo Chiarini on Unsplash

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Reactive Programming Coroutines Channels

Slide 5

Slide 5 text

Reactive Programming

Slide 6

Slide 6 text

● Focus on the data itself ● React to changes in data ● No control over where data comes from ○ helps us avoid asynchronicity issues

Slide 7

Slide 7 text

fun getUsers(): Observable { return api.getAllUsers() // Maybe> .filter { it.isNotEmpty() } .flattenAsObservable { it } // Observable } fun getUserDetails(name: String): Maybe { return api.getUserDetails(name) } .flatMapMaybe { getUserDetails(it.name) } .toList() // Single> .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { handleDetailedUsers(it) }, { handleErrors(it) } ) getUsers()

Slide 8

Slide 8 text

.flatMapMaybe { getUserDetails(it.name) } .toList() // Single> .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { handleDetailedUsers(it) }, { handleErrors(it) } ) compositeDisposable.add(getUsers() ) fun getUsers(): Observable { return api.getAllUsers() // Maybe> .filter { it.isNotEmpty() } .flattenAsObservable { it } // Observable } fun getUserDetails(name: String): Maybe { return api.getUserDetails(name) }

Slide 9

Slide 9 text

.flatMapMaybe { getUserDetails(it.name) } .toList() // Single> .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { handleDetailedUsers(it) }, { handleErrors(it) } ) getUsers() .addTo(compositeDisposable) // Extension function fun getUsers(): Observable { return api.getAllUsers() // Maybe> .filter { it.isNotEmpty() } .flattenAsObservable { it } // Observable } fun getUserDetails(name: String): Maybe { return api.getUserDetails(name) }

Slide 10

Slide 10 text

Coroutines

Slide 11

Slide 11 text

● Lightweight threads ○ A typical thread on Android: 1 to 2mb ○ A typical coroutine on Android: a couple of bytes ● Provide mechanisms to jump between different thread types ● Use suspending functions to pause/continue execution (without blocking the thread) ● Perform async computations with sequential looking code

Slide 12

Slide 12 text

suspend fun getUsers(): List { return api.getAllUsers() } suspend fun getUserDetails(name: String): DetailedUser { return api.getUserDetails(name) } val job = scope.launch { // scope tied to the main thread withContext(Dispatchers.IO) { val users = getUsers() // List .map { async { getUserDetails(it.name) } } // Yay parallelism! .map { it.await() } // Wait for the async calls to finish } // back in the main thread if (users.isNotEmpty()) { handleDetailedUsers(users) } else { handleErrors(NoUsersError()) } }

Slide 13

Slide 13 text

Channels

Slide 14

Slide 14 text

● Synchronization primitives ● Communicate between sender and receiver through suspending operations ● Hot stream - close it, or leak it ○ channel.close()

Slide 15

Slide 15 text

val job = GlobalScope.launch { print("Everything litty, ") produce { send("I love when it's hot") } } job.invokeOnCompletion { print("Turned up the city, ") } Thread.sleep(1000) println("I broke off the notch") Everything litty, I broke off the notch

Slide 16

Slide 16 text

val channel = Channel() val job = GlobalScope.launch { print("Everything litty, ") channel.send("I love when it's hot") channel.close() } CoroutineScope(Dispatchers.IO).launch { println(channel.receive()) } job.invokeOnCompletion { print("Turned up the city, ") } Thread.sleep(1000) println("I broke off the notch") Everything litty, I love when it's hot Turned up the city, I broke off the notch

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

Kotlin Flow

Slide 19

Slide 19 text

● Asynchronously computed cold reactive streams ● Can be built through specific builders ○ flow { }, flowOf(), .asFlow(), etc ● Intermediate operators ○ map, filter, zip, flatMapMerge, etc ● Terminal operators: ○ collect, toList, toSet, launchIn, etc

Slide 20

Slide 20 text

fun notFoo(): Flow = flow { for (i in 1..3) { delay(1000) println("Emitting $i") emit(i) } } scope.launch { val flow = notFoo() println("Calling collect!") flow.collect { value -> println("Collecting $value") } println("Again!") flow.collect { value -> println("Collecting $value") } } Emitting 1 Collecting 1 Emitting 2 Collecting 2 Emitting 3 Collecting 3 Emitting 1 Collecting 1 Emitting 2 Collecting 2 Emitting 3 Collecting 3 Calling collect! Again!

Slide 21

Slide 21 text

scope.launch { notFoo() .flowOn(Dispatchers.Default) .collect { value -> println("Collecting $value") } } fun notFoo(): Flow = flow { for (i in 1..3) { delay(1000) println("Emitting $i") emit(i) } }

Slide 22

Slide 22 text

launch(Dispatchers.Main) { notFoo() } .filter { it % 2 == 0 } // Main // Main

Slide 23

Slide 23 text

launch(Dispatchers.Main) { notFoo() } .filter { it % 2 == 0 } .flowOn(Dispatchers.IO) // IO // IO

Slide 24

Slide 24 text

launch(Dispatchers.Main) { notFoo() } .filter { it % 2 == 0 } .flowOn(Dispatchers.IO) // IO // IO .map { it * 2 } // Main

Slide 25

Slide 25 text

launch(Dispatchers.Main) { notFoo() } .filter { it % 2 == 0 } .flowOn(Dispatchers.IO) // IO // IO .map { it * 2 } // Default .flowOn(Dispatchers.Default)

Slide 26

Slide 26 text

launch(Dispatchers.Main) { notFoo() } .filter { it % 2 == 0 } .flowOn(Dispatchers.IO) // IO // IO .map { it * 2 } // Default .flowOn(Dispatchers.Default) .collect { value -> println("Collecting $value") } // Main

Slide 27

Slide 27 text

Buffer 1 2 3 Collected in 1220 ms fun main() = runBlocking { val time = measureTimeMillis { notFoo().collect { value -> delay(300) // “processing” it for 300 ms println(value) } } println("Collected in $time ms") } fun notFoo(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are waiting 100 ms emit(i) // emit next value } }

Slide 28

Slide 28 text

Buffer fun notFoo(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are waiting 100 ms emit(i) // emit next value } } 1 2 3 Collected in 1071 ms fun main() = runBlocking { val time = measureTimeMillis { notFoo() .buffer() // buffer emissions, don't wait .collect { value -> delay(300) // “processing” it for 300 ms println(value) } } println("Collected in $time ms") }

Slide 29

Slide 29 text

Conflate 1 3 Collected in 766 ms fun notFoo(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are waiting 100 ms emit(i) // emit next value } } fun main() = runBlocking { val time = measureTimeMillis { notFoo() .conflate() // conflate emissions, don't process each one .collect { value -> delay(300) // “processing” it for 300 ms println(value) } } println("Collected in $time ms") }

Slide 30

Slide 30 text

Latest Collecting 1 Collecting 2 Collecting 3 Done 3 Collected in 741 ms fun notFoo(): Flow = flow { for (i in 1..3) { delay(100) // pretend we are waiting 100 ms emit(i) // emit next value } } fun main() = runBlocking { val time = measureTimeMillis { notFoo() .collectLatest { value -> // cancel & restart on latest println("Collecting $value") delay(300) // pretend we are processing it for 300 ms println("Done $value") } } println("Collected in $time ms") }

Slide 31

Slide 31 text

Size limiting operators fun numbers(): Flow = flow { try { emit(1) emit(2) println("This line will not execute") emit(3) } catch (e: Exception) { println(e) } finally { println("Finally in numbers") } } fun main() = runBlocking { numbers() .take(2) // take only the first two .collect { value -> println(value) } } 1 2 kotlinx.coroutines.flow.internal. AbortFlowException: Flow was aborted, no more elements needed Finally in numbers

Slide 32

Slide 32 text

Size limiting operators fun numbers(): Flow = flow { emit(1) emit(2) emit(3) } fun main() = runBlocking { numbers() .take(2) // take only the first two .catch { e -> println(e)} .onCompletion { println("Finally in numbers") } .collect { value -> println(value) } } 1 2 Finally in numbers

Slide 33

Slide 33 text

Transform suspend fun performRequest(request: Int): String { delay(1000) // imitate long-running asynchronous work return "response $request" } fun main() = runBlocking { (1..3).asFlow() // a flow of requests .transform { request -> emit("Making request $request") emit(performRequest(request)) } .collect { response -> println(response) } } Response 1 Making request 2 Making request 1 Response 2 Making request 3 Response 3

Slide 34

Slide 34 text

Flow on Android ● User interactions ● Database updates ● Wherever you need the Observer pattern

Slide 35

Slide 35 text

Flow on Android button.setOnClickListener { // do things }

Slide 36

Slide 36 text

Flow on Android fun View.clicks(): Flow = callbackFlow { // ProducerScope val listener = View.OnClickListener { offer(Unit) } setOnClickListener(listener) awaitClose { setOnClickListener(null) } } // Usage (RxBinding, anyone?) button.clicks() .map { /* apply operators */ } .collect { /* collect events */ }

Slide 37

Slide 37 text

Flow on Android @Dao interface UsersDao { @Query("SELECT * from Users") fun getAllUsers(): } List

Slide 38

Slide 38 text

Flow on Android @Dao interface UsersDao { @Query("SELECT * from Users") fun getAllUsers(): } Flow>

Slide 39

Slide 39 text

Flow on Android sealed class ViewEvent { object UpdateUsers : ViewEvent() object LoadMoreUsers : ViewEvent() }

Slide 40

Slide 40 text

Flow on Android sealed class ViewEvent { object UpdateUsers : ViewEvent() object LoadMoreUsers : ViewEvent() } // SomeActivity fun viewEvents(): Flow { val flows = listOf( updateButton.clicks().map { ViewEvent.UpdateUsers }, loadMoreButton.clicks().map { ViewEvent.LoadMoreUsers }, ) return flows.asFlow().flattenMerge(flows.size) }

Slide 41

Slide 41 text

Flow on Android sealed class ViewEvent { object UpdateUsers : ViewEvent() object LoadMoreUsers : ViewEvent() } // SomeActivity fun viewEvents(): Flow { val flows = listOf( updateButton.clicks().map { ViewEvent.UpdateUsers }, loadMoreButton.clicks().map { ViewEvent.LoadMoreUsers }, ) return flows.asFlow().flattenMerge(flows.size) } scope.launch { viewEvents() .collect { viewEvent -> viewModel.processEvent(viewEvent) } }

Slide 42

Slide 42 text

Flow on Android sealed class ViewEvent { object UpdateUsers : ViewEvent() object LoadMoreUsers : ViewEvent() } // SomeActivity fun viewEvents(): Flow { val flows = listOf( updateButton.clicks().map { ViewEvent.UpdateUsers }, loadMoreButton.clicks().map { ViewEvent.LoadMoreUsers }, ) return flows.asFlow().flattenMerge(flows.size) } viewEvents() .onEach { viewEvent -> viewModel.processEvent(viewEvent) } .launchIn(scope)

Slide 43

Slide 43 text

Flow on Android data class ViewState( val isLoading: Boolean = false, val users: List = emptyList(), val possibleFailure: Failure = Failure.NoFailure )

Slide 44

Slide 44 text

Flow on Android data class ViewState( val isLoading: Boolean = false, val users: List = emptyList(), val possibleFailure: Failure = Failure.NoFailure ) // SomeViewModel private val _state = ConflatedBroadcastChannel() val stateFlow = _state.asFlow()

Slide 45

Slide 45 text

Flow on Android data class ViewState( val isLoading: Boolean = false, val users: List = emptyList(), val possibleFailure: Failure = Failure.NoFailure ) // SomeViewModel private val _state = ConflatedBroadcastChannel() val stateFlow = _state.asFlow() val newState: ViewState = /** Compute new state, according to view event */

Slide 46

Slide 46 text

Flow on Android data class ViewState( val isLoading: Boolean = false, val users: List = emptyList(), val possibleFailure: Failure = Failure.NoFailure ) // SomeViewModel private val _state = ConflatedBroadcastChannel() val stateFlow = _state.asFlow() val newState: ViewState = /** Compute new state, according to view event */ _state.offer(newState)

Slide 47

Slide 47 text

Flow on Android data class ViewState( val isLoading: Boolean = false, val users: List = emptyList(), val possibleFailure: Failure = Failure.NoFailure ) // SomeViewModel private val _state = ConflatedBroadcastChannel() val stateFlow = _state.asFlow() val newState: ViewState = /** Compute new state, according to view event */ _state.offer(newState) // Collect in Activity stateFlow .onEach { state -> updateUI(state) } .launchIn(scope)

Slide 48

Slide 48 text

Resources ● Kotlin Flow official documentation - https://kotlinlang.org/docs/reference/coroutines/flow.html ● Roman Elizarov’s Medium posts about Flow - https://medium.com/@elizarov/cold-flows-hot-channels-d74769805f9 ● Roman Elizarov’s talk at Kotlinconf 2019 - https://www.youtube.com/watch?v=E4F0YU8Jd5g&t=4895s ● David Karnok’s Kotlin Flow Extensions - https://github.com/akarnokd/kotlin-flow-extensions ● Lessons Learnt using Coroutines Flow in the Android Dev Summit 2019 App - https://medium.com/androiddevelopers/lessons-learnt-using-coroutines-flow-4a6b285c0d06 ● Android Dev Summit 2019 App repo - https://github.com/google/iosched/tree/adssched ● “Coroutine + Flow = MVI” by Etienne Caron at Droidcon NYC 2019 - https://www.droidcon.com/media-detail?video=362742098 ● “Flowing Things, not so strange in the MVI world” by Garima Jain at Droidcon NYC 2019 - https://www.droidcon.com/media-detail?video=362742238

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

Do you even (Kotlin) Flow? The new API for Reactive Programming Ricardo Costeira @rcosteira79 Photo by Riccardo Chiarini on Unsplash Thank you!