Slide 1

Slide 1 text

Fueled Reactive apps with Asynchronous Flow & StateFlow to Sync with the UI @raulhernandezl

Slide 2

Slide 2 text

Software Engineer Raul Hernandez Lopez @ Twitter raulh82vlc @raulhernandezl

Slide 3

Slide 3 text

Agenda @raulhernandezl

Slide 4

Slide 4 text

1. Use case 2. Migration strategy 3. Basics 4. Implementation 5. Lessons learned 6. Next steps @raulhernandezl AGENDA

Slide 5

Slide 5 text

Use case @raulhernandezl

Slide 6

Slide 6 text

Tweets Search sample app Search for Tweets with the #hashtag @raulhernandezl

Slide 7

Slide 7 text

Migration Strategy @raulhernandezl

Slide 8

Slide 8 text

Legacy means Refactoring @raulhernandezl

Slide 9

Slide 9 text

Steps to follow @raulhernandezl

Slide 10

Slide 10 text

Analysing previous Architecture @raulhernandezl

Slide 11

Slide 11 text

Repository Network data source DB data source Data Layer @raulhernandezl

Slide 12

Slide 12 text

Use Case Repository Network data source DB data source Business Layer @raulhernandezl requests results results

Slide 13

Slide 13 text

Presenter Use Case Repository Network data source DB data source Model View Presenter (MVP) + Clean Architecture @raulhernandezl requests executes results results results

Slide 14

Slide 14 text

Presenter Use Case Repository View / Callbacks Network data source DB data source Model View Presenter (MVP) + Clean Architecture @raulhernandezl View Delegate View Listener starts injects requests executes types results results results

Slide 15

Slide 15 text

Check backwards compatibility requirements @raulhernandezl

Slide 16

Slide 16 text

@raulhernandezl

Slide 17

Slide 17 text

Old & New need to co-exist together by the time being @raulhernandezl

Slide 18

Slide 18 text

Analysing pinpoints & connections @raulhernandezl

Slide 19

Slide 19 text

Presenter Use Case Repository View / Callbacks Network data source DB data source requests View Delegate View Listener JAVA + RXJAVA @raulhernandezl executes starts injects types results results results

Slide 20

Slide 20 text

Presenter Use Case Repository View / Callbacks Network data source DB data source requests executes View / Callbacks View Delegate View Listener MIGRATION of DATASOURCES: KOTLIN + RXJAVA @raulhernandezl starts injects types results results results

Slide 21

Slide 21 text

Presenter Use Case Repository Network data source DB data source requests executes View / Callbacks View Delegate View Listener MIGRATION of DATASOURCES: KOTLIN + RXJAVA + COROUTINES @raulhernandezl starts injects types results results results

Slide 22

Slide 22 text

Presenter Use Case Repository Network data source DB data source requests executes View / Callbacks View Delegate View Listener MIGRATION of REPOSITORY: KOTLIN + RXJAVA @raulhernandezl starts injects types results results results

Slide 23

Slide 23 text

Presenter Use Case Repository View / Callbacks Network data source DB data source requests executes View / Callbacks View Delegate View Listener MIGRATION of REPOSITORY: KOTLIN + RXJAVA + FLOW @raulhernandezl starts injects types results results results

Slide 24

Slide 24 text

Presenter Use Case Repository View / Callbacks Network data source DB data source requests executes View / Callbacks View Delegate View Listener MIGRATION of REPOSITORY: KOTLIN + FLOW / SUSPEND @raulhernandezl starts injects types results results results Flow

Slide 25

Slide 25 text

Presenter Use Case Repository Network data source DB data source requests executes View / Callbacks View Delegate View Listener MIGRATION of USE CASE: KOTLIN + FLOW / SUSPEND @raulhernandezl starts injects types results results results Flow Flow

Slide 26

Slide 26 text

Presenter Use Case Repository Network data source DB data source requests executes Flow Flow View / Callbacks View Delegate View Listener @raulhernandezl CAN WE CHANGE VIEWDELEGATE’S RXJAVA? starts injects types results results results

Slide 27

Slide 27 text

Presenter Use Case Repository View / Callbacks Network data source DB data source View Delegate starts executes requests View Listener Channels as Flows injects VIEWDELEGATE: CHANNELS @raulhernandezl Flow Flow types results results results

Slide 28

Slide 28 text

Presenter Use Case Repository View / Callbacks Network data source DB data source View Delegate starts executes requests View Listener Channels as Flows injects CAN WE REMOVE CALLBACKS to COMMUNICATE? @raulhernandezl Flow Flow types results results results

Slide 29

Slide 29 text

Presenter Use Case Repository View Network data source DB data source View Delegate starts executes requests View Listener Channels as Flows injects CALLBACKS MIGRATION: STATEFLOW @raulhernandezl Flow Flow results types StateFlow StateFlow

Slide 30

Slide 30 text

Presenter Use Case Repository View Network data source DB data source View Delegate starts executes requests View Listener Channels as Flows injects CALLBACKS MIGRATION: STATEFLOW HANDLER @raulhernandezl Flow Flow StateFlow StateFlow Handler results types StateFlow initializes processes

Slide 31

Slide 31 text

Benefits @raulhernandezl

Slide 32

Slide 32 text

Re-usage of existent elements @raulhernandezl

Slide 33

Slide 33 text

Layers collaboration @raulhernandezl

Slide 34

Slide 34 text

Basics @raulhernandezl

Slide 35

Slide 35 text

What is an Open Stream? @raulhernandezl

Slide 36

Slide 36 text

Open Streams are conversations like Raul Cristina ACK 1..n ACK 1..n @raulhernandezl

Slide 37

Slide 37 text

What if open streams are NOT needed? @raulhernandezl

Slide 38

Slide 38 text

ONE SHOT OPERATIONS @raulhernandezl SINGLE Single SUSPEND FUNCTION OBJECT suspend () -> T

Slide 39

Slide 39 text

ONE SHOT OPERATIONS @raulhernandezl SINGLE Single SUSPEND FUNCTION OBJECT suspend () -> T MAYBE Maybe SUSPEND FUNCTION NULLABLE suspend () -> T?

Slide 40

Slide 40 text

ONE SHOT OPERATIONS @raulhernandezl SINGLE Single SUSPEND FUNCTION OBJECT suspend () -> T MAYBE Maybe SUSPEND FUNCTION NULLABLE suspend () -> T? COMPLETABLE Completable SUSPEND FUNCTION UNIT suspend () -> Unit

Slide 41

Slide 41 text

Threading @raulhernandezl

Slide 42

Slide 42 text

THREADING SCHEDULER @raulhernandezl

Slide 43

Slide 43 text

THREADING SCHEDULER DISPATCHER @raulhernandezl

Slide 44

Slide 44 text

Lifecycle @raulhernandezl

Slide 45

Slide 45 text

LIFECYCLE DISPOSABLE @raulhernandezl

Slide 46

Slide 46 text

LIFECYCLE DISPOSABLE SCOPE @raulhernandezl

Slide 47

Slide 47 text

SCOPE STRUCTURED CONCURRENCY @raulhernandezl

Slide 48

Slide 48 text

STRUCTURED CONCURRENCY RECURSIVE CLEAN UP @raulhernandezl

Slide 49

Slide 49 text

SCOPE PROCESS 3 PROCESS 2 CANCELLATION PROCESS 1 @raulhernandezl

Slide 50

Slide 50 text

SCOPE PROCESS 3 PROCESS 2 CANCELLATION PROCESS 1 @raulhernandezl

Slide 51

Slide 51 text

CANCELLATION RECURSIVE CLEAN UP AVOIDS MEMORY LEAKS @raulhernandezl

Slide 52

Slide 52 text

AVOID SIDE-EFFECTS ON CANCELLATION SCOPE + SupervisorJob @raulhernandezl

Slide 53

Slide 53 text

AVOID SIDE-EFFECTS ON CANCELLATION SCOPE + SupervisorJob AVOID OTHER CHILDREN CANCELLATION @raulhernandezl

Slide 54

Slide 54 text

SCOPE + SupervisorJob PROCESS 3 PROCESS 2 STRUCTURED CONCURRENCY CANCELLATION PROCESS 1 @raulhernandezl

Slide 55

Slide 55 text

Implementation @raulhernandezl

Slide 56

Slide 56 text

Gradle dependencies @raulhernandezl build.gradle ... // Kotlin Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" // Kotlin Standard Library implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_stdlib_version" kotlin_coroutines_version = '1.4.0' kotlin_stdlib_version = '1.4.0'

Slide 57

Slide 57 text

Data Sources Design: Network & DB @raulhernandezl

Slide 58

Slide 58 text

Presenter Use Case Repository Network data source DB data source requests executes View / Callbacks View Delegate View Listener SUSPEND to SINGLE @raulhernandezl starts injects results results types results

Slide 59

Slide 59 text

@Singleton class NetworkDataSourceImpl @Inject constructor( private val twitterApi: TwitterApi, private val connectionHandler: ConnectionHandler, private val requestsIOHandler: RequestsIOHandler ) : NetworkDataSource NetworkDataSourceImpl constructor dependencies @raulhernandezl

Slide 60

Slide 60 text

@NotNull public Single> search( @NotNull String token, @NotNull String query ) { return twitterApi.search( requestsIOHandler.getTokenFormatted(token),query) ... } NetworkDataSource w/ Single (Java) @raulhernandezl

Slide 61

Slide 61 text

@NotNull public Single> search( @NotNull String token, @NotNull String query ) { return twitterApi.search( requestsIOHandler.getTokenFormatted(token),query) .filter(requestsIOHandler::searchHasNoErrorResponse) .map(requestsIOHandler::getTweets) .flatMapSingle(Single::just)); } NetworkDataSource w/ Single (Java) @raulhernandezl

Slide 62

Slide 62 text

override suspend fun search(token: String, query: String) : Either> { ... } NetworkDataSource w/ suspend @raulhernandezl

Slide 63

Slide 63 text

override suspend fun search(token: String, query: String) : Either> { val response = twitterApi.search( requestsIOHandler.getTokenFormatted(token), query) return if (requestsIOHandler.searchIsSuccessful(response)) { val tweets = requestsIOHandler.getTweets(response) Either.right(tweets) } else { Either.left(EmptyResponseException(response.msg())) } } NetworkDataSource w/ suspend @raulhernandezl

Slide 64

Slide 64 text

override fun search(token: String, query: String) : Single>> { return rxSingle { val response = twitterApi.search( requestsIOHandler.getTokenFormatted(token), query) if (requestsIOHandler.searchIsSuccessful(response)) { val tweets = requestsIOHandler.getTweets(response) Either.right(tweets) } else { Either.left(EmptyResponseException()) } } } NetworkDataSource with... rxSingle @raulhernandezl kotlinx-coroutines-rx2

Slide 65

Slide 65 text

@Query("SELECT * FROM ${Tweet.TABLE_NAME} WHERE tweet_id IN(:tweetIds) ORDER BY created_at DESC") fun retrieveAllTweetsRxForTweetsIds(tweetIds: List): Single> TweetDao: Database datasource (DAO) @raulhernandezl

Slide 66

Slide 66 text

Repository Design Step 1: Learning (or Naive) RxJava approach @raulhernandezl

Slide 67

Slide 67 text

Presenter Use Case Repository View / Callbacks Network data source DB data source requests executes View / Callbacks View Delegate View Listener REPOSITORY: KOTLIN + FLOW + RXJAVA @raulhernandezl starts injects types results results types results

Slide 68

Slide 68 text

@Singleton class TweetsRepositoryImpl @Inject constructor( private val networkDataSource: NetworkDataSource, private val tweetsDataSource: TweetDao, private val mapperTweets: TweetsNetworkToDBMapperList, private val tokenDataSource: TokenDao, private val queryDataSource: QueryDao, private val tweetQueryJoinDataSource: TweetQueryJoinDao, private val mapperToken: TokenNetworkToDBMapper, private val taskThreading: TaskThreading ) : TweetsRepository { TweetsRepository constructor dependencies @raulhernandezl

Slide 69

Slide 69 text

fun getSearchTweets(token: String, query: String): Observable> { return networkDataSource.search(token, query) .subscribeOn(taskThreading.io()) .observeOn(taskThreading.computation()) ... } TweetsRepository: Kotlin + RxJava threading

Slide 70

Slide 70 text

fun getSearchTweets(token: String, query: String): Observable> { return networkDataSource.search(token, query) .subscribeOn(taskThreading.io()) .observeOn(taskThreading.computation()) .map { either -> mapperTweets.map(either) } ... } TweetsRepository: Kotlin + RxJava map

Slide 71

Slide 71 text

fun getSearchTweets(token: String, query: String): Observable> { return networkDataSource.search(token, query) .subscribeOn(taskThreading.io()) .observeOn(taskThreading.computation()) .map { either -> mapperTweets.map(either) } .doOnNext { tweetsToAdd -> tweetQueryJoinDataSource.deleteTweets(query) tweetsDataSource.insertTweets(tweetsToAdd) insertQuery(query) tweetQueryJoinDataSource.insertQueryAndTweetsId( convert(tweetsToAdd, query)) ... } TweetsRepository: Kotlin + RxJava doOnNext

Slide 72

Slide 72 text

fun getSearchTweets(token: String, query: String): Observable> { return networkDataSource.search(token, query) .subscribeOn(taskThreading.io()) .observeOn(taskThreading.computation()) .map { either -> mapperTweets.map(either) } .doOnNext { tweetsToAdd -> tweetQueryJoinDataSource.deleteTweets(query) tweetsDataSource.insertTweets(tweetsToAdd) insertQuery(query) tweetQueryJoinDataSource.insertQueryAndTweetsId( convert(tweetsToAdd, query)) }.flatMap { tweetQueryJoinDataSource.retrieveAllTweetsIdRxAQuery(query) .flatMap { tweetIds -> tweetsDataSource .retrieveAllTweetsRxForTweetsIds(tweetIds) } } } TweetsRepository: Kotlin + RxJava flatMap

Slide 73

Slide 73 text

fun getSearchTweets(token: String, query: String): Flow> { return networkDataSource .search(token, query) .map { either -> mapperTweets.map(either) } ... } TweetsRepository w/ Flow @raulhernandezl

Slide 74

Slide 74 text

fun getSearchTweets(token: String, query: String): Flow> { return networkDataSource .search(token, query) .map { either -> mapperTweets.map(either) } .onEach { tweetQueryJoinDataSource.deleteTweets(query) tweetsDataSource.insertTweets(it) insertQuery(query) tweetQueryJoinDataSource.insertQueryAndTweetsId( convert(it,query)) } ... } TweetsRepository w/ onEach @raulhernandezl

Slide 75

Slide 75 text

fun getSearchTweets(token: String, query: String): Flow> { return networkDataSource .search(token, query) .map { either -> mapperTweets.map(either) } .onEach { tweetQueryJoinDataSource.deleteTweets(query) tweetsDataSource.insertTweets(it) insertQuery(query) tweetQueryJoinDataSource.insertQueryAndTweetsId( convert(it,query)) }.flatMapConcat { val tweetIds = tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) return@flatMapConcat tweetsDataSource .retrieveAllTweetsForTweetsIdsFlow(tweetIds) } } TweetsRepository w/ flatMapConcat @raulhernandezl

Slide 76

Slide 76 text

@Query("SELECT * FROM ${Tweet.TABLE_NAME} WHERE tweet_id IN(:tweetIds) ORDER BY created_at DESC") fun retrieveAllTweetsForTweetsIdsFlow(tweetIds: List): Flow> Database datasource (DAO) w/ Flow @raulhernandezl

Slide 77

Slide 77 text

fun getSearchTweets(token: String, query: String): Flow> { return networkDataSource .search(token, query) .map { either -> mapperTweets.map(either) } .onEach { tweetQueryJoinDataSource.deleteTweets(query) tweetsDataSource.insertTweets(it) insertQuery(query) tweetQueryJoinDataSource.insertQueryAndTweetsId( convert(it,query)) }.flatMapConcat { val tweetIds = tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) return@flatMapConcat tweetsDataSource .retrieveAllTweetsForTweetsIdsFlow(tweetIds) }.flowOn(taskThreading.ioDispatcher()) } TweetsRepository w/ flowOn & IO dispatcher @raulhernandezl

Slide 78

Slide 78 text

class TaskThreadingImpl @Inject constructor() : TaskThreading { private val computationScheduler = Schedulers.computation() private val ioScheduler = Schedulers.io() private val mainScheduler = AndroidSchedulers.mainThread() override fun ui(): Scheduler = mainScheduler override fun io(): Scheduler = ioScheduler override fun computation(): Scheduler = computationScheduler } TaskThreading with RxJava Schedulers @raulhernandezl

Slide 79

Slide 79 text

class TaskThreadingImpl @Inject constructor() : TaskThreading { private val computationCoroutineDispatcher: CoroutineDispatcher = Default private val ioCoroutineDispatcher: CoroutineDispatcher = IO private val mainCoroutineDispatcher: CoroutineDispatcher = Main ... override fun uiDispatcher(): CoroutineDispatcher = mainCoroutineDispatcher override fun ioDispatcher(): CoroutineDispatcher = ioCoroutineDispatcher override fun computationDispatcher(): CoroutineDispatcher = computationCoroutineDispatcher } TaskThreading with Coroutines Dispatchers @raulhernandezl

Slide 80

Slide 80 text

@Singleton class TaskThreadingImpl @Inject constructor() : TaskThreading { private val computationScheduler = Schedulers.computation() private val ioScheduler = Schedulers.io() private val mainScheduler = AndroidSchedulers.mainThread() ... } Can we re-use same RxJava schedulers / dispatchers? @raulhernandezl

Slide 81

Slide 81 text

@Singleton class TaskThreadingImpl @Inject constructor() : TaskThreading { private val computationScheduler = Schedulers.computation() private val ioScheduler = Schedulers.io() private val mainScheduler = AndroidSchedulers.mainThread() ... } Yes, we can re-use existing RxJava schedulers YES! @raulhernandezl

Slide 82

Slide 82 text

Scheduler.asCoroutineDispatcher -> Converts scheduler to CoroutineDispatcher asCoroutineDispatcher kotlinx-coroutines-rx2 @raulhernandezl

Slide 83

Slide 83 text

@Singleton class TaskThreadingImpl @Inject constructor() : TaskThreading { private val computationScheduler = Schedulers.computation() private val ioScheduler = Schedulers.io() private val mainScheduler = AndroidSchedulers.mainThread() override fun uiDispatcher(): CoroutineDispatcher = mainScheduler.asCoroutineDispatcher() override fun ioDispatcher(): CoroutineDispatcher = ioScheduler.asCoroutineDispatcher() override fun computationDispatcher(): CoroutineDispatcher = computationScheduler.asCoroutineDispatcher() TaskThreading: reuse Schedulers transformed into CoroutineDispatchers kotlinx-coroutines-rx2 @raulhernandezl

Slide 84

Slide 84 text

fun getSearchTweets(token: String, query: String): Observable> { return networkDataSource .search(token, query) .map { either -> mapperTweets.map(either) } .onEach { tweetQueryJoinDataSource.deleteTweets(query) tweetsDataSource.insertTweets(it) insertQuery(query) tweetQueryJoinDataSource.insertQueryAndTweetsId( convert(it,query)) }.flatMapConcat { val tweetIds = tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) return@flatMapConcat tweetsDataSource .retrieveAllTweetsForTweetsIdsFlow(tweetIds) }.flowOn(taskThreading.ioDispatcher()).asObservable() } TweetsRepository w/ Flow to Observable @raulhernandezl kotlinx-coroutines-rx2

Slide 85

Slide 85 text

Repository Design Step 2: (Pro) Suspend approach @raulhernandezl

Slide 86

Slide 86 text

Presenter Use Case Repository View / Callbacks Network data source DB data source requests executes View / Callbacks View Delegate View Listener REPOSITORY: KOTLIN + FLOW / SUSPEND @raulhernandezl starts injects results results types results

Slide 87

Slide 87 text

suspend fun getSearchTweets(query: String): List { val accessToken = tokenDataSource.getAccessToken() ?: throw BadTokenException("No token was found") val token = accessToken.token ... } Case 1) TweetsRepository: suspend @raulhernandezl

Slide 88

Slide 88 text

suspend fun getSearchTweets(query: String): List { val accessToken = tokenDataSource.getAccessToken() ?: throw BadTokenException("No token was found") val token = accessToken.token val eitherResult = networkDataSource .search(token, query) val tweets = mapperTweets.map(eitherResult) ... } Case 1) TweetsRepository: suspend @raulhernandezl

Slide 89

Slide 89 text

suspend fun getSearchTweets(query: String): List { val accessToken = tokenDataSource.getAccessToken() ?: throw BadTokenException("No token was found") val token = accessToken.token val eitherResult = networkDataSource .search(token, query) val tweets = mapperTweets.map(eitherResult) tweetQueryJoinDataSource.deleteTweets(query) tweetsDataSource.insertTweets(tweets) insertQuery(query) tweetQueryJoinDataSource.insertQueryAndTweetsId( tweetQueryJoinDataSource.convert(tweets, query)) ... } Case 1) TweetsRepository: suspend @raulhernandezl

Slide 90

Slide 90 text

suspend fun getSearchTweets(query: String): List { val accessToken = tokenDataSource.getAccessToken() ?: throw BadTokenException("No token was found") val token = accessToken.token val eitherResult = networkDataSource .search(token, query) val tweets = mapperTweets.map(eitherResult) tweetQueryJoinDataSource.deleteTweets(query) tweetsDataSource.insertTweets(tweets) insertQuery(query) tweetQueryJoinDataSource.insertQueryAndTweetsId( tweetQueryJoinDataSource.convert(tweets, query)) val tweetIds = tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) return tweetsDataSource.retrieveAllTweetsForTweetsIdsFlow(tweetIds) } @raulhernandezl Case 1) TweetsRepository: Can Flow be returned into a suspend function? ???

Slide 91

Slide 91 text

suspend fun getSearchTweets(query: String): List { val accessToken = tokenDataSource.getAccessToken() ?: throw BadTokenException("No token was found") val token = accessToken.token val eitherResult = networkDataSource .search(token, query) val tweets = mapperTweets.map(eitherResult) tweetQueryJoinDataSource.deleteTweets(query) tweetsDataSource.insertTweets(tweets) insertQuery(query) tweetQueryJoinDataSource.insertQueryAndTweetsId( tweetQueryJoinDataSource.convert(tweets, query)) val tweetIds = tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) return tweetsDataSource.retrieveAllTweetsForTweetsIdsFlow(tweetIds) } Case 1) TweetsRepository: Flow cannot be returned into suspend function! @raulhernandezl NO!

Slide 92

Slide 92 text

suspend fun getSearchTweets(query: String): List { val accessToken = tokenDataSource.getAccessToken() ?: throw BadTokenException("No token was found") val token = accessToken.token val eitherResult = networkDataSource .search(token, query) val tweets = mapperTweets.map(eitherResult) tweetQueryJoinDataSource.deleteTweets(query) tweetsDataSource.insertTweets(tweets) insertQuery(query) tweetQueryJoinDataSource.insertQueryAndTweetsId( tweetQueryJoinDataSource.convert(tweets, query)) val tweetIds = tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) return tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds) } Case 1) TweetsRepository: a suspend function is needed @raulhernandezl YES!

Slide 93

Slide 93 text

@Query("SELECT * FROM ${Tweet.TABLE_NAME} WHERE tweet_id IN(:tweetIds) ORDER BY created_at DESC") suspend fun retrieveAllTweetsForTweetsIds(tweetIds: List): List Database datasource (DAO) with suspend @raulhernandezl

Slide 94

Slide 94 text

Case 2) TweetsRepository: Flow @raulhernandezl

Slide 95

Slide 95 text

fun getSearchTweets(query: String): Flow> = flow { ... val tweetIds = tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) emitAll(tweetsDataSource.retrieveAllTweetsForTweetsIdsFlow(tweetIds)) }.flowOn(taskThreading.ioDispatcher()) Case 2) TweetsRepository: flow builder & emitAll values from DB (Flow) @raulhernandezl YES!

Slide 96

Slide 96 text

fun getSearchTweets(query: String): Flow> = flow { ... val tweetIds = tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) emit(tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds)) }.flowOn(taskThreading.ioDispatcher()) Case 2) TweetsRepository: flow builder & emit a suspend fun returning a List @raulhernandezl YES!

Slide 97

Slide 97 text

fun getSearchTweets(query: String): Flow> = flow { ... // retrieve old values from DB emit(tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds)) // get fresh values from network & saved them into DB val eitherResult = networkDataSource.search(token, query) ... // saved network into DB & emit fresh values from DB val tweetIds = tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) emit(tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds)) }.flowOn(taskThreading.ioDispatcher()) Case 2) TweetsRepository: flow builder & emit more than once @raulhernandezl YES!

Slide 98

Slide 98 text

fun getSearchTweets(query: String): Observable> = flow { ... }.flowOn(taskThreading.ioDispatcher()).asObservable() Case 2) Repository: Flow to Observable @raulhernandezl kotlinx-coroutines-rx2

Slide 99

Slide 99 text

Use Case Design @raulhernandezl

Slide 100

Slide 100 text

Presenter Use Case Repository Network data source DB data source requests executes View / Callbacks View Delegate View Listener RxSearch USE CASE: KOTLIN + FLOW / SUSPEND @raulhernandezl starts injects results results types results

Slide 101

Slide 101 text

@RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, private val taskThreading: TaskThreading ) : UseCase { SearchTweetUseCase: constructor dependencies @raulhernandezl

Slide 102

Slide 102 text

UseCase contract w/ callback interface UseCase { fun execute(param: String, callbackInput: T?) fun cancel() } @raulhernandezl

Slide 103

Slide 103 text

@Override void execute(@NotNull String query, @Nullable SearchCallback callback) { this.callback = callback; if(callback != null) { callback.onShowLoader(); } } SearchTweetUseCase: RxJava + Java @raulhernandezl

Slide 104

Slide 104 text

@Override void execute(@NotNull String query, @Nullable SearchCallback callback) { this.callback = callback; if(callback != null) { callback.onShowLoader(); } repository.searchTweet(query) .subscribe( callback::onSuccess,// action for each stream callback::onError); // error handling } SearchTweetUseCase: RxJava + Java @raulhernandezl

Slide 105

Slide 105 text

@Override void execute(@NotNull String query, @Nullable SearchCallback callback) { this.callback = callback; if(callback != null) { callback.onShowLoader(); } disposable = repository.searchTweet(query) .subscribeOn(taskThreading.computation()) // subscribed to .observeOn(taskThreading.ui()) // action performed at .map( // some computation ) .subscribe( callback::onSuccess,// action for each stream callback::onError); // error handling } SearchTweetUseCase: Rx threading & map downstream @raulhernandezl

Slide 106

Slide 106 text

override fun execute(query: String, callbackInput: SearchCallback?) { callback = callbackInput callback?.onShowLoader() ... } SearchTweetUseCase: Kotlin @raulhernandezl

Slide 107

Slide 107 text

override fun execute(query: String, callbackInput: SearchCallback?) { callback = callbackInput callback?.onShowLoader() scope.launch { ... } } SearchTweetUseCase: Kotlin Flow w/ scope.launch @raulhernandezl private val scope = CoroutineScope(taskThreading.uiDispatcher() + SupervisorJob())

Slide 108

Slide 108 text

override fun execute(query: String, callbackInput: SearchCallback?) { callback = callbackInput callback?.onShowLoader() scope.launch { repository.searchTweet(query) .map { // some computation } .flowOn(taskThreading.computationDispatcher()) ... } } SearchTweetUseCase: Kotlin Flow map in another thread w/ flowOn upstream @raulhernandezl private val scope = CoroutineScope(taskThreading.uiDispatcher() + SupervisorJob())

Slide 109

Slide 109 text

override fun execute(query: String, callbackInput: SearchCallback?) { callback = callbackInput callback?.onShowLoader() scope.launch { repository.searchTweet(query) .map { // some computation } .flowOn(taskThreading.computationDispatcher()) .catch { callback::onError }.collect { tweets ->// UI actions for each stream callback.onSuccess(tweets) } } } SearchTweetUseCase: Flow + Kotlin using collect @raulhernandezl private val scope = CoroutineScope(taskThreading.uiDispatcher() + SupervisorJob())

Slide 110

Slide 110 text

override fun execute(query: String, callbackInput: SearchCallback?) { callback = callbackInput callback?.onShowLoader() repository.searchTweet(query) .map { // some computation } .flowOn(taskThreading.computationDispatcher()) .onEach { tweets -> // UI actions for each stream callback.onSuccess(tweets) }.catch { callback::onError }.launchIn(scope) } SearchTweetUseCase: Flow + Kotlin using launchIn @raulhernandezl private val scope = CoroutineScope(taskThreading.uiDispatcher() + SupervisorJob())

Slide 111

Slide 111 text

override fun cancel() { ... } private val scope = CoroutineScope(taskThreading.uiDispatcher() + SupervisorJob()) SearchTweetUseCase: How to cancel with Structured concurrency? @raulhernandezl

Slide 112

Slide 112 text

override fun cancel() { callback = null scope.cancel() } private val scope = CoroutineScope(taskThreading.uiDispatcher() + SupervisorJob()) SearchTweetUseCase: cancellation with Structured concurrency @raulhernandezl

Slide 113

Slide 113 text

SearchTweetUseCase: How to cancel observables? .subscribeOn(taskThreading.computation()) .observeOn(taskThreading.ui()) @raulhernandezl @Override public void cancel() { ... }

Slide 114

Slide 114 text

@Override public void cancel() { if (disposable != null && !disposable.isDisposed()) { disposable.dispose(); disposable = null; } callback = null; } SearchTweetUseCase: observables disposal .subscribeOn(taskThreading.computation()) .observeOn(taskThreading.ui()) @raulhernandezl

Slide 115

Slide 115 text

fun getSearchTweets(query: String): Flow> = flow { ... }.flowOn(taskThreading.ioDispatcher()).asObserv.. Reminder: TweetsRepository doesn’t need to return Observable anymore, just Flow @raulhernandezl

Slide 116

Slide 116 text

View Delegate Design @raulhernandezl

Slide 117

Slide 117 text

Presenter Use Case Repository View / Callbacks Network data source DB data source View Delegate starts executes requests View Listener Flows Flows Flows Channels as Flows injects VIEW DELEGATE with CHANNELS @raulhernandezl starts injects results results types results

Slide 118

Slide 118 text

@ActivityScope class SearchViewDelegate constructor( private val presenter: SearchTweetPresenter ) { private val publishSubject: PublishSubject = PublishSubject.create() ... } SearchViewDelegate: RxJava + Kotlin with PublishSubject @raulhernandezl

Slide 119

Slide 119 text

@ActivityScope class SearchViewDelegate constructor( private val presenter: SearchTweetPresenter ) { private val publishSubject: PublishSubject = PublishSubject.create() fun prepareViewDelegateListener(val view: SearchView) { val listener = SearchViewListener(publishSubject) view.setOnQueryTextListener(listener) } } SearchViewDelegate: RxJava + Kotlin with PublishSubject @raulhernandezl

Slide 120

Slide 120 text

@ActivityScope class SearchViewDelegate constructor( private val presenter: SearchTweetPresenter ) { fun prepareViewDelegateListener(val view: SearchView): Flow= ... } SearchViewDelegate: Flows with channels as flows @raulhernandezl

Slide 121

Slide 121 text

@ActivityScope class SearchViewDelegate constructor( private val presenter: SearchTweetPresenter ) { @ExperimentalCoroutinesApi fun prepareViewDelegateListener(val view: SearchView): Flow = (channelFlow { // define listener val listener = SearchViewListener(channel) ... }).flowOn(taskThreading.ioDispatcher()) } @raulhernandezl SearchViewDelegate: Flows with channelFlow builder

Slide 122

Slide 122 text

@ExperimentalCoroutinesApi fun prepareViewDelegateListener(val view: SearchView): Flow = (channelFlow { val listener = SearchViewListener(channel) view.setOnQueryTextListener(listener) awaitClose { view.setOnQueryTextListener(null) } }).flowOn(taskThreading.ioDispatcher()) SearchViewDelegate: Flows setting/closing the query listener @raulhernandezl

Slide 123

Slide 123 text

@ActivityScope class SearchViewDelegate constructor( private val presenter: SearchTweetPresenter ) { fun observeSubject(): Disposable { ... } fun cancel() { view.setOnQueryTextListener(null) if (disposable != null && !disposable.isDisposed()) { disposable.dispose() disposable = null } presenter.cancel() } } SearchViewDelegate: RxJava needs to manually dispose the view and disposable @raulhernandezl

Slide 124

Slide 124 text

class SearchViewListener constructor( private val publishSubject: PublishSubject ): SearchView.OnQueryTextListener { override fun onQueryTextChange(query: String): Boolean { ... return true } override fun onQueryTextSubmit(query: String) = false } SearchViewListener w/ injected PublishSubject @raulhernandezl

Slide 125

Slide 125 text

class SearchViewListener constructor( private val publishSubject: PublishSubject ): SearchView.OnQueryTextListener { override fun onQueryTextChange(query: String): Boolean { publishSubject.onNext(query) return true } override fun onQueryTextSubmit(query: String) = false } SearchViewListener: RxJava with publishSubject @raulhernandezl

Slide 126

Slide 126 text

class SearchViewListener constructor( private val queryChannel: SendChannel ): SearchView.OnQueryTextListener { override fun onQueryTextChange(query: String): Boolean { ... return true } override fun onQueryTextSubmit(query: String) = false } SearchViewListener: Channels with SendChannel @raulhernandezl

Slide 127

Slide 127 text

class SearchViewListener constructor( private val queryChannel: SendChannel ): SearchView.OnQueryTextListener { override fun onQueryTextChange(query: String): Boolean { queryChannel.offer(query) return true } override fun onQueryTextSubmit(query: String) = false } SearchViewListener: Channels with SendChannel @raulhernandezl

Slide 128

Slide 128 text

@ActivityScope class SearchViewDelegate constructor( private val presenter: SearchTweetPresenter ) { fun observeSubject(): Disposable = publishSubject .observeOn(taskThreading.ui()) .subscribeOn(taskThreading.computation()) ... } SearchViewDelegate w/ RxJava subject @raulhernandezl

Slide 129

Slide 129 text

@ActivityScope class SearchViewDelegate constructor( private val presenter: SearchTweetPresenter ) { fun observeSubject(): Disposable = publishSubject .observeOn(taskThreading.ui()) .subscribeOn(taskThreading.computation()) .debounce(600, TimeUnit.MILLISECONDS) .distinctUntilChanged() .filter { query -> filterQuery(query) } .subscribe ( ... ) } SearchViewDelegate w/ RxJava Subject downstream @raulhernandezl

Slide 130

Slide 130 text

@ActivityScope class SearchViewDelegate constructor( private val presenter: SearchTweetPresenter ) { fun observeSubject(): Disposable = publishSubject .observeOn(taskThreading.ui()) .subscribeOn(taskThreading.computation()) .debounce(600, TimeUnit.MILLISECONDS) .distinctUntilChanged() .filter { query -> filterQuery(query) } .subscribe ( presenter::searchTweet, presenter::showProblemHappened ) } SearchViewDelegate w/ RxJava starts Presenter @raulhernandezl

Slide 131

Slide 131 text

@FlowPreview fun observeChannelAsFlow() { declareViewDelegate() .debounce(600) .distinctUntilChanged() .filter { query -> filterQuery(query) } .flowOn(taskThreading.computationDispatcher()) ... } SearchViewDelegate: w/ Flow flowOn upstream @raulhernandezl

Slide 132

Slide 132 text

@FlowPreview fun observeChannelAsFlow() { declareViewDelegate() .debounce(600) .distinctUntilChanged() .filter { query -> filterQuery(query) } .flowOn(taskThreading.computationDispatcher()) .onEach { query -> presenter.searchTweet(query) } .catch { presenter::showProblemHappened } .launchIn(scope) } fun cancel() { scope.cancel() presenter.cancel() } SearchViewDelegate lifecycle handling and stream @raulhernandezl private val scope = CoroutineScope(taskThreading.uiDispatcher() + SupervisorJob())

Slide 133

Slide 133 text

All done to get rid of RxJava? @raulhernandezl

Slide 134

Slide 134 text

NO! @raulhernandezl All done to get rid of RxJava?

Slide 135

Slide 135 text

TaskThreading Migration @raulhernandezl

Slide 136

Slide 136 text

class TaskThreadingImpl @Inject constructor() : TaskThreading { private val computationScheduler = Schedulers.computation() private val ioScheduler = Schedulers.io() private val mainScheduler = AndroidSchedulers.mainThread() private val computationCoroutineContext: CoroutineDispatcher = Default private val ioCoroutineContext: CoroutineDispatcher = IO private val mainCoroutineContext: CoroutineDispatcher = Main Use native Coroutine Dispatchers @raulhernandezl

Slide 137

Slide 137 text

class TaskThreadingImpl @Inject constructor() : TaskThreading { private val computationCoroutineContext: CoroutineDispatcher = Default private val ioCoroutineContext: CoroutineDispatcher = IO private val mainCoroutineContext: CoroutineDispatcher = Main override fun ui(): CoroutineContext = mainCoroutineContext override fun io(): CoroutineContext = ioCoroutineContext override fun computation(): CoroutineContext = computationCoroutineContext } Expose new dispatchers @raulhernandezl

Slide 138

Slide 138 text

Synchronous communication Design @raulhernandezl

Slide 139

Slide 139 text

Presenter Use Case Repository View Network data source DB data source View Delegate starts executes requests View Listener Flows Channels as Flows injects @raulhernandezl Flows Flows results types StateFlow StateFlow STATEFLOW to get rid of CALLBACKS in USE CASE

Slide 140

Slide 140 text

@RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, private val taskThreading: TaskThreading ) : UseCase { ) : UseCaseFlow { SearchTweetUseCase recap: constructor dependencies @raulhernandezl

Slide 141

Slide 141 text

SearchTweetUseCase update: remove callback interface UseCaseStateFlow { fun execute(param: String, callbackInput: SearchCallback?) fun cancel() fun getStateFlow(): StateFlow } @raulhernandezl

Slide 142

Slide 142 text

SearchTweetUseCase update: remove callback interface UseCaseStateFlow { fun execute(param: String, callbackInput: SearchCallback?) fun cancel() fun getStateFlow(): StateFlow } @raulhernandezl StateFlow can be passed across Java & Kotlin files

Slide 143

Slide 143 text

@RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, private val taskThreading: TaskThreading ) : UseCaseFlow { private val tweetsUIStateFlow = MutableStateFlow(null) override fun getStateFlow(): StateFlow = tweetsUIStateFlow SearchTweetUseCase w/ MutableStateFlow @raulhernandezl

Slide 144

Slide 144 text

@RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, private val taskThreading: TaskThreading ) : UseCaseFlow { private val tweetsUIStateFlow = MutableStateFlow(null) override fun getStateFlow(): StateFlow = tweetsUIStateFlow SearchTweetUseCase w/ MutableStateFlow distinctUntilChanged by default @raulhernandezl StateFlow uses distinctUntilChanged by default

Slide 145

Slide 145 text

TweetsUIState w/ Results state @raulhernandezl /** * UI States defined for StateFlow in the workflow */ sealed class TweetsUIState { data class ListResultsUIState(val tweets: List): TweetsUIState() }

Slide 146

Slide 146 text

TweetsUIState w/ Error state @raulhernandezl /** * UI States defined for StateFlow in the workflow */ sealed class TweetsUIState { data class ListResultsUIState(val tweets: List): TweetsUIState() data class ErrorUIState(val msg: String): TweetsUIState() }

Slide 147

Slide 147 text

TweetsUIState w/ Empty state @raulhernandezl /** * UI States defined for StateFlow in the workflow */ sealed class TweetsUIState { data class ListResultsUIState(val tweets: List): TweetsUIState() data class ErrorUIState(val msg: String): TweetsUIState() object EmptyUIState: TweetsUIState() }

Slide 148

Slide 148 text

TweetsUIState w/ Loading state @raulhernandezl /** * UI States defined for StateFlow in the workflow */ sealed class TweetsUIState { data class ListResultsUIState(val tweets: List): TweetsUIState() data class ErrorUIState(val msg: String): TweetsUIState() object EmptyUIState: TweetsUIState() object LoadingUIState: TweetsUIState() }

Slide 149

Slide 149 text

override fun execute(query: String) { callback?.onShowLoader() tweetsStateFlow.value = TweetsUIState.LoadingUIState } SearchTweetUseCase: Loading state propagation @raulhernandezl

Slide 150

Slide 150 text

override fun execute(query: String) { tweetsStateFlow.value = TweetsUIState.LoadingUIState repository.searchTweet(query) .onEach { tweets -> val stateFlow = if (tweets.isEmpty()) { TweetsUIState.EmptyUIState } tweetsStateFlow.value = stateFlow } .catch { e -> ... }.launchIn(scope) } SearchTweetUseCase: Empty state propagation @raulhernandezl

Slide 151

Slide 151 text

override fun execute(query: String) { tweetsStateFlow.value = TweetsUIState.LoadingUIState repository.searchTweet(query) .onEach { tweets -> val stateFlow = if (tweets.isEmpty()) { TweetsUIState.EmptyUIState } else { TweetsUIState.ListResultsUIState(tweets) } tweetsStateFlow.value = stateFlow } .catch { e -> ... }.launchIn(scope) } SearchTweetUseCase: List results state propagation @raulhernandezl

Slide 152

Slide 152 text

override fun execute(query: String) { tweetsStateFlow.value = TweetsUIState.LoadingUIState repository.searchTweet(query) .onEach { tweets -> val stateFlow = if (tweets.isEmpty()) { TweetsUIState.EmptyUIState } else { TweetsUIState.ListResultsUIState(tweets) } tweetsStateFlow.value = stateFlow } .catch { e -> tweetsStateFlow.value = TweetsUIState.ErrorUIState(e.msg)) }.launchIn(scope) } SearchTweetUseCase: Error state propagation @raulhernandezl

Slide 153

Slide 153 text

Presenter Use Case Repository View Network data source DB data source View Delegate starts executes requests View Listener Flows Channels as Flows injects STATEHANDLER (KOTLIN) to COLLECT STATE @raulhernandezl Flows Flows StateFlow StateFlow Handler results types StateFlow initializes processes

Slide 154

Slide 154 text

SearchTweetPresenter (Java): constructor @raulhernandezl @ActivityScope public class SearchTweetPresenter { @NotNull private final SearchTweetUseCase tweetSearchUseCase; @Inject public SearchTweetPresenter( @NotNull SearchTweetUseCase tweetSearchUseCase ) { this.tweetSearchUseCase = tweetSearchUseCase; } public void cancel() { tweetSearchUseCase.cancel(); }

Slide 155

Slide 155 text

SearchTweetPresenter (Java) responsibilities @raulhernandezl @ActivityScope public class SearchTweetPresenter { ... public void searchTweets(@NotNull final String query) { if (callback == null && view != null) { callback = new SearchCallbackImpl(view); } tweetSearchUseCase.execute(query, callback); } @NotNull public StateFlow getStateFlow() { return tweetSearchUseCase.getStateFlow(); } StateFlow can be passed across Java & Kotlin files

Slide 156

Slide 156 text

SearchViewDelegate gets StateFlow from Presenter @raulhernandezl @ActivityScope class SearchViewDelegate @Inject constructor( private val presenter: SearchTweetPresenter, private val taskThreading: TaskThreading ) { private fun getStateFlow(): StateFlow = presenter.stateFlow

Slide 157

Slide 157 text

SearchViewDelegate initialises UI w/ StateFlow @raulhernandezl @ActivityScope class SearchViewDelegate @Inject constructor( private val presenter: SearchTweetPresenter, private val taskThreading: TaskThreading ) { private fun getStateFlow(): StateFlow = presenter.stateFlow fun initialiseProcessingQuery( searchView: SearchView, uiResults: TweetsListUI? ) { uiResults?.apply { initStateFlow(getStateFlow()) } ... }

Slide 158

Slide 158 text

TweetsListUI / View (Java) delegates to StateFlowHandler @raulhernandezl public class TweetsListUI extends ViewUI { @Inject SearchViewDelegate viewDelegate; @Inject StateFlowHandler stateFlowHandler; public void initStateFlow( @NotNull StateFlow extends TweetsUIState> stateFlow ) { stateFlowHandler.init(stateFlow, this); stateFlowHandler.processStateFlowCollection(); } StateFlow can be passed across Java & Kotlin files

Slide 159

Slide 159 text

StateFlowHandler: constructor @raulhernandezl @ActivityScope class StateFlowHandler @Inject constructor( taskThreading: TaskThreading ) { private var tweetsListUI: TweetsListUI? = null private lateinit var stateFlow: StateFlow fun init( val stateFlow: StateFlow, val tweetsListUI: TweetsListUI ) { stateFlow = stateFlow tweetsListUI = tweetsListUI } ... }

Slide 160

Slide 160 text

StateFlowHandler filters StateFlow prior collection private lateinit var stateFlow: StateFlow fun processStateCollection() { stateFlow .filter { stateFlow -> stateFlow != null } ... } @raulhernandezl

Slide 161

Slide 161 text

StateFlowHandler collects StateFlow w/ onEach private lateinit var stateFlow: StateFlow fun processStateCollection() { stateFlow .filter { stateFlow -> stateFlow != null } .onEach { stateFlow -> tweetsListUI?.handleStates(stateFlow) }... } @raulhernandezl

Slide 162

Slide 162 text

StateFlowHandler w/ StateFlow collection requires a scope private val scope = CoroutineScope(taskThreading.ui() + SupervisorJob()) fun processStateCollection() { stateFlow .filter { stateFlow -> stateFlow != null } .onEach { stateFlow -> tweetsListUI?.handleStates(stateFlow) }.launchIn(scope) } @raulhernandezl StateFlow needs a Scope to be collected

Slide 163

Slide 163 text

TweetsListUI handles States @raulhernandezl fun TweetsListUI.handleStates(stateFlow: TweetsUIState?) { when (stateFlow) { is TweetsUIState.LoadingUIState -> { showLoading() } is TweetsUIState.ListResultsUIState -> { showResults(stateFlow) } is TweetsUIState.EmptyUIState -> { showEmptyState() } is TweetsUIState.ErrorUIState -> { showError(stateFlow) } else -> { showEmptyState() } } }

Slide 164

Slide 164 text

StateFlowHandler cancels StateFlow collection @raulhernandezl @ActivityScope class StateFlowHandler @Inject constructor( taskThreading: TaskThreading ) { private val scope = CoroutineScope(taskThreading.ui() + SupervisorJob()) private var tweetsListUI: TweetsListUI? = null fun cancel() { scope.cancel() tweetsListUI = null }

Slide 165

Slide 165 text

TweetsListUI / View lifecycle @raulhernandezl public class TweetsListUI extends ViewUI { @Inject SearchViewDelegate viewDelegate; @Inject StateFlowHandler stateFlowHandler; @Override public void onDestroy() { stateFlowHandler.cancel(); viewDelegate.cancel(); super.onDestroy(); } }

Slide 166

Slide 166 text

Fueled Reactive with Flow done! @raulhernandezl

Slide 167

Slide 167 text

Lessons learned @raulhernandezl

Slide 168

Slide 168 text

Exchangeable @raulhernandezl

Slide 169

Slide 169 text

Structured Concurrency @raulhernandezl

Slide 170

Slide 170 text

Structured Concurrency is gone when using Observables and Dispatchers simultaneously @raulhernandezl

Slide 171

Slide 171 text

Imperative vs Declarative programming @raulhernandezl

Slide 172

Slide 172 text

Pros & Cons @raulhernandezl RxJava Declarative Coroutines + Flow Imperative + Declarative RxJava Manual lifecycle handling *by default Coroutines + Flow Structured Concurrency RxJava Java + Kotlin hybrid projects * Coroutines + Flow Perfect for Kotlin & friends

Slide 173

Slide 173 text

Next Steps @raulhernandezl

Slide 174

Slide 174 text

Wait for stable Flow APIs @raulhernandezl

Slide 175

Slide 175 text

Wait for stable Flow APIs @Deprecated @raulhernandezl

Slide 176

Slide 176 text

References @raulhernandezl Fueled Reactive apps with Asynchronous Flow 1. Use case & Migration Strategy 2. Asynchronous communication: Streams & Basics 3. Data layer Implementation 4. Use Case layer Implementation 5. View Delegate Implementation 6. Lessons learned & Next steps 7. Synchronous communication with the UI using StateFlow

Slide 177

Slide 177 text

Special thanks @raulhernandezl Manuel Vivo @manuelvicnt

Slide 178

Slide 178 text

Thank you. @raulhernandezl

Slide 179

Slide 179 text

Questions? @raulhernandezl