Fueled Reactive apps with Asynchronous Flow & Stateflow to Sync with the UI v2

Fueled Reactive apps with Asynchronous Flow & Stateflow to Sync with the UI v2

Reactive Extensions are widely used on large scale successful Android applications, and for this, the most popular library is an adaptation of these Reactive Extensions, the very well known RxJava.
What if we could use the existing Kotlin suspending functions asynchronously to return more than a single value?
How could we return multiple asynchronously backed results in Kotlin?
Kotlin Flows to the rescue!
Would Kotlin Flows replace RxJava in the long term on the Android platform?
How could we start migrating smoothly to Flow on hybrid (Java & Kotlin) languages apps?
Can we fully avoid using callbacks now? StateFlow to the rescue!

Conference or meetup:
- Droidcon EMEA #dcEMEA #droidconEMEA (October 2020)
- Madrid Android Developer Group (MADG) meetup - Spanish language (November 2020)
- Kotlin London meetup (December 2020)

Ffc500baeba9a1024e2c8273203c9f90?s=128

Raul Hernandez Lopez

October 09, 2020
Tweet

Transcript

  1. Fueled Reactive apps with Asynchronous Flow & Stateflow to Synchronously

    communicate with the UI @raulhernandezl
  2. Software Engineer Raul Hernandez Lopez @ Twitter raulh82vlc @raulhernandezl

  3. Agenda @raulhernandezl

  4. 1. Use case 2. Migration strategy 3. Basics 4. Implementation

    5. Lessons learned 6. Next steps @raulhernandezl AGENDA
  5. Use case @raulhernandezl

  6. Tweets Search sample app Search for Tweets with the #hashtag

    @raulhernandezl
  7. Migration Strategy @raulhernandezl

  8. Legacy means Refactoring @raulhernandezl

  9. Steps to follow @raulhernandezl

  10. Analysing previous Architecture @raulhernandezl

  11. Repository Network data source DB data source Data Layer @raulhernandezl

  12. Use Case Repository Network data source DB data source Business

    Layer @raulhernandezl requests results results
  13. Presenter Use Case Repository Network data source DB data source

    Model View Presenter (MVP) + Clean Architecture @raulhernandezl requests executes results results results
  14. 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
  15. Check backwards compatibility requirements @raulhernandezl

  16. @raulhernandezl

  17. Old & New need to co-exist together by the time

    being @raulhernandezl
  18. Analysing pinpoints & connections @raulhernandezl

  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. Benefits @raulhernandezl

  32. Re-usage of existent elements @raulhernandezl

  33. Layers collaboration @raulhernandezl

  34. Basics @raulhernandezl

  35. What is an Open Stream? @raulhernandezl

  36. Open Streams are conversations like Raul Cristina ACK 1..n ACK

    1..n @raulhernandezl
  37. What if open streams are NOT needed? @raulhernandezl

  38. ONE SHOT OPERATIONS @raulhernandezl SINGLE Single<T> SUSPEND FUNCTION OBJECT suspend

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

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

    () -> T MAYBE Maybe<T> SUSPEND FUNCTION NULLABLE suspend () -> T? COMPLETABLE Completable SUSPEND FUNCTION UNIT suspend () -> Unit
  41. Threading @raulhernandezl

  42. THREADING SCHEDULER @raulhernandezl

  43. THREADING SCHEDULER DISPATCHER @raulhernandezl

  44. Lifecycle @raulhernandezl

  45. LIFECYCLE DISPOSABLE @raulhernandezl

  46. LIFECYCLE DISPOSABLE SCOPE @raulhernandezl

  47. SCOPE STRUCTURED CONCURRENCY @raulhernandezl

  48. STRUCTURED CONCURRENCY RECURSIVE CLEAN UP @raulhernandezl

  49. SCOPE PROCESS 3 PROCESS 2 CANCELLATION PROCESS 1 @raulhernandezl

  50. SCOPE PROCESS 3 PROCESS 2 CANCELLATION PROCESS 1 @raulhernandezl

  51. CANCELLATION RECURSIVE CLEAN UP AVOIDS MEMORY LEAKS @raulhernandezl

  52. AVOID SIDE-EFFECTS ON CANCELLATION SCOPE + SupervisorJob @raulhernandezl

  53. AVOID SIDE-EFFECTS ON CANCELLATION SCOPE + SupervisorJob AVOID OTHER CHILDREN

    CANCELLATION @raulhernandezl
  54. SCOPE + SupervisorJob PROCESS 3 PROCESS 2 STRUCTURED CONCURRENCY CANCELLATION

    PROCESS 1 @raulhernandezl
  55. Implementation @raulhernandezl

  56. 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'
  57. Data Sources Design: Network & DB @raulhernandezl

  58. 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
  59. @Singleton class NetworkDataSourceImpl @Inject constructor( private val twitterApi: TwitterApi, private

    val connectionHandler: ConnectionHandler, private val requestsIOHandler: RequestsIOHandler ) : NetworkDataSource NetworkDataSourceImpl constructor dependencies @raulhernandezl
  60. @NotNull public Single<List<TweetApiModel>> search( @NotNull String token, @NotNull String query

    ) { return twitterApi.search( requestsIOHandler.getTokenFormatted(token),query) ... } NetworkDataSource w/ Single (Java) @raulhernandezl
  61. @NotNull public Single<List<TweetApiModel>> 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
  62. override suspend fun search(token: String, query: String) : Either<Throwable, List<TweetApiModel>>

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

    { 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
  64. override fun search(token: String, query: String) : Single<Either<Throwable, List<TweetApiModel>>> {

    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
  65. @Query("SELECT * FROM ${Tweet.TABLE_NAME} WHERE tweet_id IN(:tweetIds) ORDER BY created_at

    DESC") fun retrieveAllTweetsRxForTweetsIds(tweetIds: List<String>): Single<List<Tweet>> TweetDao: Database datasource (DAO) @raulhernandezl
  66. Repository Design Step 1: Learning (or Naive) RxJava approach @raulhernandezl

  67. 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
  68. @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
  69. fun getSearchTweets(token: String, query: String): Observable<List<Tweet>> { return networkDataSource.search(token, query)

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

    .subscribeOn(taskThreading.io()) .observeOn(taskThreading.computation()) .map { either -> mapperTweets.map(either) } ... } TweetsRepository: Kotlin + RxJava map
  71. fun getSearchTweets(token: String, query: String): Observable<List<Tweet>> { 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
  72. fun getSearchTweets(token: String, query: String): Observable<List<Tweet>> { 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
  73. fun getSearchTweets(token: String, query: String): Flow<List<Tweet>> { return networkDataSource .search(token,

    query) .map { either -> mapperTweets.map(either) } ... } TweetsRepository w/ Flow @raulhernandezl
  74. fun getSearchTweets(token: String, query: String): Flow<List<Tweet>> { 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
  75. fun getSearchTweets(token: String, query: String): Flow<List<Tweet>> { 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
  76. @Query("SELECT * FROM ${Tweet.TABLE_NAME} WHERE tweet_id IN(:tweetIds) ORDER BY created_at

    DESC") fun retrieveAllTweetsForTweetsIdsFlow(tweetIds: List<String>): Flow<List<Tweet>> Database datasource (DAO) w/ Flow @raulhernandezl
  77. fun getSearchTweets(token: String, query: String): Flow<List<Tweet>> { 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
  78. 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
  79. 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
  80. @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
  81. @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
  82. Scheduler.asCoroutineDispatcher -> Converts scheduler to CoroutineDispatcher asCoroutineDispatcher kotlinx-coroutines-rx2 @raulhernandezl

  83. @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
  84. fun getSearchTweets(token: String, query: String): Observable<List<Tweet>> { 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
  85. Repository Design Step 2: (Pro) Suspend approach @raulhernandezl

  86. 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
  87. suspend fun getSearchTweets(query: String): List<Tweet> { val accessToken = tokenDataSource.getAccessToken()

    ?: throw BadTokenException("No token was found") val token = accessToken.token ... } Case 1) TweetsRepository: suspend @raulhernandezl
  88. suspend fun getSearchTweets(query: String): List<Tweet> { 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
  89. suspend fun getSearchTweets(query: String): List<Tweet> { 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
  90. suspend fun getSearchTweets(query: String): List<Tweet> { 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? ???
  91. suspend fun getSearchTweets(query: String): List<Tweet> { 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!
  92. suspend fun getSearchTweets(query: String): List<Tweet> { 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!
  93. @Query("SELECT * FROM ${Tweet.TABLE_NAME} WHERE tweet_id IN(:tweetIds) ORDER BY created_at

    DESC") suspend fun retrieveAllTweetsForTweetsIds(tweetIds: List<String>): List<Tweet> Database datasource (DAO) with suspend @raulhernandezl
  94. Case 2) TweetsRepository: Flow @raulhernandezl

  95. fun getSearchTweets(query: String): Flow<List<Tweet>> = 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!
  96. fun getSearchTweets(query: String): Flow<List<Tweet>> = 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!
  97. fun getSearchTweets(query: String): Flow<List<Tweet>> = 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!
  98. fun getSearchTweets(query: String): Observable<List<Tweet>> = flow { ... }.flowOn(taskThreading.ioDispatcher()).asObservable() Case

    2) Repository: Flow to Observable @raulhernandezl kotlinx-coroutines-rx2
  99. Use Case Design @raulhernandezl

  100. 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
  101. @RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, private

    val taskThreading: TaskThreading ) : UseCase<SearchCallback> { SearchTweetUseCase: constructor dependencies @raulhernandezl
  102. UseCase contract w/ callback interface UseCase<T> { fun execute(param: String,

    callbackInput: T?) fun cancel() } @raulhernandezl
  103. @Override void execute(@NotNull String query, @Nullable SearchCallback callback) { this.callback

    = callback; if(callback != null) { callback.onShowLoader(); } } SearchTweetUseCase: RxJava + Java @raulhernandezl
  104. @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
  105. @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
  106. override fun execute(query: String, callbackInput: SearchCallback?) { callback = callbackInput

    callback?.onShowLoader() ... } SearchTweetUseCase: Kotlin @raulhernandezl
  107. 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())
  108. 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())
  109. 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())
  110. 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())
  111. override fun cancel() { ... } private val scope =

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

    val scope = CoroutineScope(taskThreading.uiDispatcher() + SupervisorJob()) SearchTweetUseCase: cancellation with Structured concurrency @raulhernandezl
  113. SearchTweetUseCase: How to cancel observables? .subscribeOn(taskThreading.computation()) .observeOn(taskThreading.ui()) @raulhernandezl @Override public

    void cancel() { ... }
  114. @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
  115. fun getSearchTweets(query: String): Flow<List<Tweet>> = flow { ... }.flowOn(taskThreading.ioDispatcher()).asObserv.. Reminder:

    TweetsRepository doesn’t need to return Observable anymore, just Flow @raulhernandezl
  116. View Delegate Design @raulhernandezl

  117. 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
  118. @ActivityScope class SearchViewDelegate constructor( private val presenter: SearchTweetPresenter ) {

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

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

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

    @ExperimentalCoroutinesApi fun prepareViewDelegateListener(val view: SearchView): Flow<String> = (channelFlow<String> { // define listener val listener = SearchViewListener(channel) ... }).flowOn(taskThreading.ioDispatcher()) } @raulhernandezl SearchViewDelegate: Flows with channelFlow builder
  122. @ExperimentalCoroutinesApi fun prepareViewDelegateListener(val view: SearchView): Flow<String> = (channelFlow<String> { val

    listener = SearchViewListener(channel) view.setOnQueryTextListener(listener) awaitClose { view.setOnQueryTextListener(null) } }).flowOn(taskThreading.ioDispatcher()) SearchViewDelegate: Flows setting/closing the query listener @raulhernandezl
  123. @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
  124. class SearchViewListener constructor( private val publishSubject: PublishSubject<String> ): SearchView.OnQueryTextListener {

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

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

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

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

    fun observeSubject(): Disposable = publishSubject .observeOn(taskThreading.ui()) .subscribeOn(taskThreading.computation()) ... } SearchViewDelegate w/ RxJava subject @raulhernandezl
  129. @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
  130. @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
  131. @FlowPreview fun observeChannelAsFlow() { declareViewDelegate() .debounce(600) .distinctUntilChanged() .filter { query

    -> filterQuery(query) } .flowOn(taskThreading.computationDispatcher()) ... } SearchViewDelegate: w/ Flow flowOn upstream @raulhernandezl
  132. @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())
  133. All done to get rid of RxJava? @raulhernandezl

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

  135. TaskThreading Migration @raulhernandezl

  136. 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
  137. 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
  138. Synchronous communication Design @raulhernandezl

  139. 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
  140. @RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, private

    val taskThreading: TaskThreading ) : UseCase<SearchCallback> { ) : UseCaseFlow { SearchTweetUseCase recap: constructor dependencies @raulhernandezl
  141. SearchTweetUseCase update: remove callback interface UseCaseStateFlow { fun execute(param: String,

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

    callbackInput: SearchCallback?) fun cancel() fun getStateFlow(): StateFlow<TweetsUIState?> } @raulhernandezl StateFlow can be passed across Java & Kotlin files
  143. @RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, private

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

    val taskThreading: TaskThreading ) : UseCaseFlow { private val tweetsUIStateFlow = MutableStateFlow<TweetsUIState?>(null) override fun getStateFlow(): StateFlow<TweetsUIState?> = tweetsUIStateFlow SearchTweetUseCase w/ MutableStateFlow distinctUntilChanged by default @raulhernandezl StateFlow uses distinctUntilChanged by default
  145. TweetsUIState w/ Results state @raulhernandezl /** * UI States defined

    for StateFlow in the workflow */ sealed class TweetsUIState { data class ListResultsUIState(val tweets: List<Tweet>): TweetsUIState() }
  146. TweetsUIState w/ Error state @raulhernandezl /** * UI States defined

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

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

    for StateFlow in the workflow */ sealed class TweetsUIState { data class ListResultsUIState(val tweets: List<Tweet>): TweetsUIState() data class ErrorUIState(val msg: String): TweetsUIState() object EmptyUIState: TweetsUIState() object LoadingUIState: TweetsUIState() }
  149. override fun execute(query: String) { callback?.onShowLoader() tweetsStateFlow.value = TweetsUIState.LoadingUIState }

    SearchTweetUseCase: Loading state propagation @raulhernandezl
  150. 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
  151. 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
  152. 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
  153. 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
  154. 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(); }
  155. 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<TweetsUIState> getStateFlow() { return tweetSearchUseCase.getStateFlow(); } StateFlow can be passed across Java & Kotlin files
  156. SearchViewDelegate gets StateFlow from Presenter @raulhernandezl @ActivityScope class SearchViewDelegate @Inject

    constructor( private val presenter: SearchTweetPresenter, private val taskThreading: TaskThreading ) { private fun getStateFlow(): StateFlow<TweetsUIState> = presenter.stateFlow
  157. SearchViewDelegate initialises UI w/ StateFlow @raulhernandezl @ActivityScope class SearchViewDelegate @Inject

    constructor( private val presenter: SearchTweetPresenter, private val taskThreading: TaskThreading ) { private fun getStateFlow(): StateFlow<TweetsUIState> = presenter.stateFlow @FlowPreview @ExperimentalCoroutinesApi fun initialiseProcessingQuery( searchView: SearchView, uiResults: TweetsListUI? ) { uiResults?.apply { initStateFlow(getStateFlow()) } ... }
  158. 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
  159. StateFlowHandler: constructor @raulhernandezl @ActivityScope class StateFlowHandler @Inject constructor( taskThreading: TaskThreading

    ) { private var tweetsListUI: TweetsListUI? = null private lateinit var stateFlow: StateFlow<TweetsUIState?> fun init( val stateFlow: StateFlow<TweetsUIState?>, val tweetsListUI: TweetsListUI ) { stateFlow = stateFlow tweetsListUI = tweetsListUI } ... }
  160. StateFlowHandler filters StateFlow prior collection private lateinit var stateFlow: StateFlow<TweetsUIState?>

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

    fun processStateCollection() { stateFlow .filter { stateFlow -> stateFlow != null } .onEach { stateFlow -> tweetsListUI?.handleStates(stateFlow) }... } @raulhernandezl
  162. 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
  163. 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() } } }
  164. 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 }
  165. 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(); } }
  166. Fueled Reactive with Flow done! @raulhernandezl

  167. Lessons learned @raulhernandezl

  168. Exchangeable @raulhernandezl

  169. Structured Concurrency @raulhernandezl

  170. Structured Concurrency is gone when using Observables and Dispatchers simultaneously

    @raulhernandezl
  171. Imperative vs Declarative programming @raulhernandezl

  172. 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
  173. Next Steps @raulhernandezl

  174. Wait for stable Flow APIs @raulhernandezl

  175. Wait for stable Flow APIs @Deprecated @raulhernandezl

  176. Special thanks @raulhernandezl Manuel Vivo @manuelvicnt

  177. Thank you. @raulhernandezl

  178. 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
  179. Questions? @raulhernandezl