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

Getting ready for Declarative UIs with Unidirectional Data Flow using Kotlin Coroutines

Getting ready for Declarative UIs with Unidirectional Data Flow using Kotlin Coroutines

Unidirectional Data Flow (UDF) is a powerful technique that enhances our Reactive apps to work deterministically. Synchronising our views with fresh data was never an easy task to accomplish. For this same reason, there are mechanisms that support us to make that possible. Surely callbacks were a thing in the past, however, they were an anti-pattern themselves due to the lack of readability. Now we don't need to deal with them any more thanks to Kotlin Coroutines. Getting ready for Declarative UIs with Kotlin Coroutines and friends is indeed feasible, now we could use suspend functions, Flow and in the end StateFlow would make our Reactive apps ready for Declarative UIs. Let’s define a single entry point, receive data, transform it into a state, and render each state. Let’s get our apps ready for a Declarative UI world on Android.

Key takeaways:
You'll learn how to use Kotlin Coroutines and friends from the Kotlin Coroutines library to take advantage of really efficient and easy to read code. How to handle its lifecycle without being compromised to a specific external Android framework, which would enable your code to be prepared for more purposes than Android only apps.

Conferences or meetups:
- FOSDEM conf (February the 7th 2021)
- Virtual Kotlin (KUG) April meetup (April the 29th 2021)
- RockNDroid Vigo May meetup (May the 12th 2021)
- Kotlin London (KUG) June meetup (June the 2nd 2021)
- DevDays Europe conf (June the 8th 2021)

Ffc500baeba9a1024e2c8273203c9f90?s=128

Raul Hernandez Lopez

February 07, 2021
Tweet

Transcript

  1. Getting ready for Declarative UIs with Unidirectional Data Flow using

    Kotlin Coroutines @raulhernandezl
  2. Software Engineer Raul Hernandez Lopez @ Twitter raulh82vlc @raulhernandezl

  3. Agenda @raulhernandezl

  4. 1. Use case 2. Analyse an existing architecture 3. Adopting

    Unidirectional Data Flow 4. Implementation 5. Lessons learned 6. Why Compose? 7. Next steps @raulhernandezl AGENDA
  5. Use case @raulhernandezl

  6. Tweets Search sample app @raulhernandezl • Loading spinner • Empty

    results text • List of results • Error message text
  7. Analyse an existing architecture @raulhernandezl

  8. Start adopting on an existing Architecture @raulhernandezl

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

    results results results • Repository manages data sources
  10. Use Case Repository Business Layer @raulhernandezl requests results • Use

    Case performs any business logic and returns values callback (results) transforms
  11. Presenter Use Case Model View Presenter (MVP) + Clean Architecture

    @raulhernandezl requests executes callback (results) results callback (results) • Presenter connects business logic with views
  12. Presenter View View connection with Presenter @raulhernandezl executes callback (results)

    callback (results) • View receives new data
  13. View User intention @raulhernandezl • The user starts typing a

    query of interest types a new query
  14. Presenter View System interaction @raulhernandezl View Delegate View Listener starts

    query injects types a new query callback (results) • Starting a search query • React to different queries
  15. Analysing Coroutines @raulhernandezl

  16. Presenter Use Case Repository View / Callbacks Network data source

    DB data source View Delegate View Listener DATA SOURCES: SUSPEND @raulhernandezl results results
  17. Presenter Use Case Repository View / Callbacks Network data source

    View Delegate requests View Listener REPOSITORY: FLOW / SUSPEND @raulhernandezl Flow results results results DB data source
  18. Presenter Use Case Repository View / Callbacks Network data source

    DB data source View Delegate executes requests View Listener USE CASE: FLOW @raulhernandezl Flow results results transforms
  19. How to get ready for Declarative UIs? @raulhernandezl

  20. Presenter Use Case Repository View / Callbacks Network data source

    DB data source CALLBACKS or NOT? @raulhernandezl results results results callback (results) callback (results)
  21. Any disadvantages on Callbacks? @raulhernandezl

  22. Lack of readability = Callback hell @raulhernandezl Callbacks into callbacks

  23. Callbacks break Referential Transparency principle @raulhernandezl

  24. Referential Transparency (RT) def. The ability to make larger functions

    out of smaller ones through composition and produce always the same output for a given input. @raulhernandezl
  25. Consequences of RT broken? @raulhernandezl

  26. Lack of a return value. @raulhernandezl Consequences of RT broken?

  27. Lack of a return value. Results are declared as input

    parameters. @raulhernandezl Consequences of RT broken?
  28. Results can mutate. @raulhernandezl

  29. Lack of Determinism Results can mutate. @raulhernandezl

  30. Are there better alternatives to Callbacks? @raulhernandezl

  31. StateFlow Always returns last value when different. @raulhernandezl

  32. StateFlow Results are immutable. @raulhernandezl

  33. StateFlow is Deterministic by default. @raulhernandezl

  34. Presenter Use Case Repository View Network data source DB data

    source View Delegate View Listener CALLBACKS -> STATEFLOW @raulhernandezl Flow results StateFlow StateFlow results results StateFlow
  35. Presenter Use Case Repository View Network data source DB data

    source View Delegate View Listener COLLECT STATEFLOW @raulhernandezl Flow Flow StateFlow StateFlow Handler results StateFlow results results StateFlow
  36. Adopting the Unidirectional Data Flow (UDF) @raulhernandezl

  37. One-way data flow @raulhernandezl

  38. @raulhernandezl UI State Action transforms results renders intends

  39. @raulhernandezl UseCase triggers action Presenter executes action Imperative UI intends

    action View Delegate ACTION
  40. @raulhernandezl Imperative UI StateFlowHandler UseCase communicates state dispatches state Imperative

    UI View Delegate renders Presenter STATE
  41. @raulhernandezl Imperative UI StateFlowHandler UseCase communicates state dispatches state Imperative

    UI View Delegate renders Presenter STATE
  42. Imperative & Declarative UIs can co-exist @raulhernandezl

  43. @raulhernandezl Imperative Declarative UI StateFlowHandler UseCase communicates state dispatches state

    Imperative UI View Delegate renders Presenter STATE
  44. Advantages @raulhernandezl

  45. Flexibility @raulhernandezl

  46. Layers composition @raulhernandezl

  47. Implementation @raulhernandezl

  48. Data Sources: Network & DB @raulhernandezl

  49. Presenter Use Case Repository View / Callbacks Network data source

    DB data source View Delegate View Listener DATA SOURCES: SUSPEND @raulhernandezl results results
  50. @Singleton class NetworkDataSourceImpl @Inject constructor( private val twitterApi: TwitterApi, private

    val connectionHandler: ConnectionHandler, private val requestsIOHandler: RequestsIOHandler ) : NetworkDataSource NetworkDataSourceImpl constructor dependencies @raulhernandezl
  51. suspend fun search(token: String, query: String) : Either<Throwable, List<TweetApiModel>> NetworkDataSource

    w/ suspend @raulhernandezl
  52. @Query("SELECT * FROM ${Tweet.TABLE_NAME} WHERE tweet_id IN(:tweetIds) ORDER BY created_at

    DESC") suspend fun retrieveAllTweetsForTweetsIds(tweetIds: List<String>): List<Tweet> TweetDao: Database datasource (DAO) w/ suspend @raulhernandezl
  53. Repository: suspend & Flow @raulhernandezl

  54. Presenter Use Case Repository View / Callbacks Network data source

    View Delegate requests View Listener REPOSITORY: FLOW / SUSPEND @raulhernandezl Flow results results results DB data source
  55. @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
  56. @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
  57. suspend fun getSearchTweets(query: String): List<Tweet> { ... val tweetIds =

    tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) return tweetsDataSource.retrieveAllTweetsForTweetsIdsFlow(tweetIds) } @raulhernandezl Case 1) TweetsRepository: Can Flow be returned into a suspend function? ???
  58. suspend fun getSearchTweets(query: String): List<Tweet> { ... val tweetIds =

    tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) return tweetsDataSource.retrieveAllTweetsForTweetsIdsFlow(tweetIds) } Case 1) TweetsRepository: Flow cannot be returned into suspend function! @raulhernandezl NO!
  59. suspend fun getSearchTweets(query: String): List<Tweet> { ... return tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds) }

    Case 1) TweetsRepository: a suspend function is needed @raulhernandezl YES!
  60. @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
  61. Case 2) TweetsRepository: Flow @raulhernandezl

  62. fun getSearchTweets(query: String): Flow<List<Tweet>> = flow { ... emitAll(tweetsDataSource.retrieveAllTweetsForTweetsIdsFlow(tweetIds)) }.flowOn(taskThreading.ioDispatcher())

    Case 2) TweetsRepository: flow builder & emitAll values from DB (Flow) @raulhernandezl YES!
  63. fun getSearchTweets(query: String): Flow<List<Tweet>> = flow { ... emit(tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds)) }.flowOn(taskThreading.ioDispatcher())

    Case 2) TweetsRepository: flow builder & emit a suspend fun returning a List @raulhernandezl YES!
  64. 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 ... // saved network into DB & emit fresh values from DB emit(tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds)) }.flowOn(taskThreading.ioDispatcher()) Case 2) TweetsRepository: flow builder & emit more than once @raulhernandezl YES!
  65. Use Case Overview @raulhernandezl

  66. Presenter Use Case Repository View / Callbacks Network data source

    DB data source View Delegate executes requests View Listener USE CASE: FLOW @raulhernandezl Flow results results transforms
  67. Presenter Use Case Repository View / Callbacks Network data source

    DB data source WHERE ARE CALLBACKS? @raulhernandezl results results results callback (results) callback (results) transforms
  68. @RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, @Named("CoroutineRetainedScope")

    private val scope: CoroutineScope ) : UseCase<SearchCallback> { SearchTweetUseCase: constructor dependencies @raulhernandezl
  69. @RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, @Named("CoroutineRetainedScope")

    private val scope: CoroutineScope ) : UseCase<SearchCallback> { SearchTweetUseCase: constructor dependencies @raulhernandezl private val scope = CoroutineScope( taskThreading.uiDispatcher() + SupervisorJob())
  70. AVOID SIDE-EFFECTS ON CANCELLATION SCOPE + SupervisorJob @raulhernandezl

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

    CANCELLATION @raulhernandezl
  72. SCOPE + SupervisorJob JOB 3 JOB 2 STRUCTURED CONCURRENCY CANCELLATION

    JOB 1 @raulhernandezl
  73. Use Case Implementation @raulhernandezl

  74. UseCase contract w/ callback interface UseCase<T> { fun execute(param: String,

    callbackInput: T?) fun cancel() } @raulhernandezl
  75. override fun execute(query: String, callbackInput: SearchCallback?) { callback = callbackInput

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

    callback?.onShowLoader() scope.launch { ... } } SearchTweetUseCase: Kotlin Flow w/ scope.launch @raulhernandezl
  77. 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
  78. 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
  79. 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
  80. override fun cancel() { callback = null scope.cancel() } SearchTweetUseCase:

    cancellation with Structured concurrency @raulhernandezl private val scope = CoroutineScope( taskThreading.uiDispatcher() + SupervisorJob())
  81. Synchronous communication with the UI @raulhernandezl

  82. Presenter Use Case Repository View Network data source DB data

    source View Delegate View Listener CALLBACKS -> STATEFLOW @raulhernandezl Flow results StateFlow StateFlow results results StateFlow
  83. @RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, @Named("CoroutineRetainedScope")

    private val scope: CoroutineScope ) : UseCase<SearchCallback> { ) : UseCaseFlow<?> { SearchTweetUseCase recap: constructor dependencies @raulhernandezl
  84. SearchTweetUseCase update: remove callback @raulhernandezl interface UseCaseFlow<T> { fun execute(param:

    String, callbackInput: SearchCallback?) fun cancel() fun getStateFlow(): StateFlow<T> }
  85. SearchTweetUseCase update: remove callback interface UseCaseFlow<T> { fun execute(param: String,

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

    private val scope: CoroutineScope ) : UseCaseFlow<TweetsUIState> { private val tweetsUIStateFlow = MutableStateFlow<TweetsUIState>(TweetsUIState.IdleUIState) override fun getStateFlow(): StateFlow<TweetsUIState> = tweetsUIStateFlow SearchTweetUseCase w/ MutableStateFlow @raulhernandezl
  87. @RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, @Named("CoroutineRetainedScope")

    private val scope: CoroutineScope ) : UseCaseFlow<TweetsUIState> { private val tweetsUIStateFlow = MutableStateFlow<TweetsUIState>(TweetsUIState.IdleUIState) override fun getStateFlow(): StateFlow<TweetsUIState> = tweetsUIStateFlow SearchTweetUseCase w/ MutableStateFlow distinctUntilChanged by default @raulhernandezl StateFlow uses distinctUntilChanged by default
  88. /** * UI States defined for StateFlow in the workflow

    */ sealed class TweetsUIState { data class ListResultsUIState(val tweets: List<Tweet>): TweetsUIState() } TweetsUIState w/ Results state @raulhernandezl
  89. TweetsUIState w/ Results state @raulhernandezl /** * UI States defined

    for StateFlow in the workflow */ sealed class TweetsUIState { data class ListResultsUIState(val tweets: List<Tweet>): TweetsUIState() } Recomposition
  90. 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() }
  91. 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() data class EmptyUIState(val query: String): TweetsUIState() }
  92. 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() data class EmptyUIState(val query: String): TweetsUIState() object LoadingUIState: TweetsUIState() }
  93. TweetsUIState w/ Idle 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() data class EmptyUIState(val query: String): TweetsUIState() object LoadingUIState: TweetsUIState() object IdleUIState: TweetsUIState() }
  94. override fun execute(query: String) { callback?.onShowLoader() repository.searchTweet(query) .onStart { tweetsStateFlow.value

    = TweetsUIState.LoadingUIState } SearchTweetUseCase: Loading state propagation @raulhernandezl
  95. override fun execute(query: String) { repository.searchTweet(query) .onStart { tweetsStateFlow.value =

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

    TweetsUIState.LoadingUIState }.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
  97. override fun execute(query: String) { repository.searchTweet(query) .onStart { tweetsStateFlow.value =

    TweetsUIState.LoadingUIState }.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
  98. Presenter Use Case Repository View Network data source DB data

    source View Delegate View Listener COLLECT STATEFLOW @raulhernandezl Flow Flow StateFlow StateFlow Handler results StateFlow results results StateFlow
  99. 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(); }
  100. 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
  101. SearchViewDelegate gets StateFlow from Presenter @raulhernandezl @ActivityScope class SearchViewDelegate @Inject

    constructor( private val presenter: SearchTweetPresenter @Named("CoroutineUIScope") private val scope: CoroutineScope ) { fun getStateFlow(): StateFlow<TweetsUIState> = presenter.stateFlow
  102. TweetsListUIFragment (Java) delegates to SearchStateHandler @raulhernandezl public class TweetsListFragmentUI extends

    BaseFragment { ... @Inject SearchViewDelegate viewDelegate; @Inject SearchStateHandler stateHandler; public void initStateFlowAndViews() { stateHandler.initStateFlowAndViews(viewDelegate.getStateFlow(), this); stateHandler.processStateFlowCollection(); } StateFlow can be passed across Java & Kotlin files
  103. TweetsListUIFragment w/ StateFlow public class TweetsListFragmentUI extends BaseFragment { //

    ... @Override public View onCreateView( @NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState ) { final View view = inflater.inflate(R.layout.fragment_search, container, false); initStateFlowAndViews() return view; } @raulhernandezl
  104. StateFlowHandler: constructor @raulhernandezl @ActivityScope class SearchStateHandler @Inject constructor( @Named("CoroutineUIScope") private

    val scope: CoroutineScope ) { private var tweetsListUI: TweetsListUIFragment? = null private lateinit var stateFlow: StateFlow<TweetsUIState> fun initStateFlowAndViews( val stateFlow: StateFlow<TweetsUIState>, val tweetsListUI: TweetsListUIFragment ) { this.stateFlow = stateFlow this.tweetsListUI = tweetsListUI } ... }
  105. SearchStateHandler collects StateFlow w/ onEach private lateinit var stateFlow: StateFlow<TweetsUIState>

    fun processStateCollection() { stateFlow .onEach { uiState -> tweetsListUI?.handleStates(uiState) }... } @raulhernandezl
  106. StateFlowHandler collection on scope for Imperative UI fun processStateCollection() {

    stateFlow .onEach { uiState -> tweetsListUI?.handleStates(uiState) }.launchIn(scope) } @raulhernandezl
  107. Imperative (Stateful) UI Rendering @raulhernandezl

  108. @raulhernandezl Imperative UI StateFlowHandler UseCase communicates state dispatches state Imperative

    UI View Delegate triggers action renders intends action Presenter executes action STATE
  109. TweetsListUIFragment handles State to show Tweets @raulhernandezl fun TweetsListFragmentUI.handleStates(uiState: TweetsUIState)

    { when (uiState) { is TweetsUIState.ListResultsUIState -> showResults(uiState.tweets) } }
  110. TweetsListUIFragment handles Stateful Loader @raulhernandezl fun TweetsListFragmentUI.showResults( tweets: List<Tweet> )

    { hideLoader() hideError() showList() updateList(tweets) } Loading data... Loading data...
  111. TweetsListUIFragment handles Stateful Error @raulhernandezl fun TweetsListFragmentUI.showResults( tweets: List<Tweet> )

    { hideLoader() hideError() showList() updateList(tweets) } There is an error for... Loading data...
  112. TweetsListUIFragment handles Stateful RecyclerView @raulhernandezl fun TweetsListFragmentUI.showResults( tweets: List<Tweet> )

    { hideLoader() hideError() showList() updateList(tweets) } RecyclerView LayoutManager Item Decorator RecyclerView
  113. TweetsListUIFragment handles Stateful RecyclerView @raulhernandezl fun TweetsListFragmentUI.showResults( tweets: List<Tweet> )

    { hideLoader() hideError() showList() updateList(tweets) } SearchStateHandler Adapter ViewHolder tweets action StateFlow TweetsUIState.ListUIState
  114. TweetsListUIFragment to show text states @raulhernandezl fun TweetsListFragmentUI.handleStates(uiState: TweetsUIState) {

    when (uiState) { is TweetsUIState.ListResultsUIState -> showResults(uiState.tweets) is TweetsUIState.LoadingUIState -> showLoading() is TweetsUIState.EmptyUIState -> showEmptyState(uiState.query) is TweetsUIState.ErrorUIState -> showError(uiState.msg) } }
  115. TweetsListUIFragment handles to do nothing on Idle @raulhernandezl fun TweetsListFragmentUI.handleStates(uiState:

    TweetsUIState) { when (uiState) { is TweetsUIState.ListResultsUIState -> showResults(uiState.tweets) is TweetsUIState.LoadingUIState -> showLoading() is TweetsUIState.EmptyUIState -> showEmptyState(uiState.query) is TweetsUIState.ErrorUIState -> showError(uiState.msg) is TweetsUIState.IdleUIState -> {} } }
  116. Declarative (Stateless) UI migration @raulhernandezl

  117. @raulhernandezl Imperative Declarative UI StateFlowHandler UseCase dispatches collected state dispatches

    state Imperative UI View Delegate triggers action renders intends action Presenter executes action STATE
  118. TweetsListUIFragment: Compose Interoperability public class TweetsListFragmentUI extends BaseFragment { @Inject

    SearchComposablesUI searchComposablesUI; @Inject SearchViewDelegate viewDelegate; @Inject SearchStateHandler stateHandler; ... } @raulhernandezl
  119. XML layout Interoperability <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- ...

    --> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_marginTop="?attr/actionBarSize" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> @raulhernandezl
  120. ComposeView Interoperability public class TweetsListUIFragment extends BaseFragment { @Override public

    View onCreateView( @NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState ) { final View view = inflater.inflate(R.layout.fragment_search, container, false); final ComposeView composeView = view.findViewById(R.id.compose_view); // ... return view; } @raulhernandezl
  121. TweetsListUIFragment ComposeView w/ StateFlow @Override public View onCreateView( @NotNull LayoutInflater

    inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState ) { final View view = inflater.inflate(R.layout.fragment_search, container, false); final ComposeView composeView = view.findViewById(R.id.compose_view); searchComposablesUI.setComposeView(composeView); ... return view; } @raulhernandezl
  122. TweetsListUIFragment ComposeView w/ StateFlow @Override public View onCreateView( @NotNull LayoutInflater

    inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState ) { final View view = inflater.inflate(R.layout.fragment_search, container, false); final ComposeView composeView = view.findViewById(R.id.compose_view); searchComposablesUI.setComposeView(composeView); return view; } @raulhernandezl fun setComposeView( composeView: ComposeView ) { this.composeView = composeView }
  123. TweetsListUIFragment delegates handling to SearchStateHandler @raulhernandezl public class TweetsListUIFragment extends

    BaseFragment { ... public void initStateFlowAndViews() { stateHandler.initStateFlowAndViews(viewDelegate.getStateFlow(), searchComposablesUI); stateHandler.processStateFlowCollection(); }
  124. SearchStateHandler collaborates with SearchComposablesUI @raulhernandezl @ActivityScope class SearchStateHandler @Inject constructor(

    @Named("CoroutineUIScope") private val scope: CoroutineScope ) { private var searchComposablesUI: SearchComposablesUI? = null private lateinit var stateFlow: StateFlow<TweetsUIState> fun initStateFlowAndViews( val stateFlow: StateFlow<TweetsUIState>, val searchComposablesUI: SearchComposablesUI ) { this.stateFlow = stateFlow this.searchComposablesUI = searchComposablesUI } ... }
  125. TweetsListUIFragment delegates to SearchStateHandler @raulhernandezl public class TweetsListUIFragment extends BaseFragment

    { ... private void initStateFlowAndViews() { stateHandler.initStateFlowAndViews( viewDelegate.getStateFlow(),searchComposablesUI); stateHandler.processStateFlowCollection(); }
  126. SearchStateHandler starts SearchComposablesUI class SearchStateHandler @Inject constructor( @Named("CoroutineUIScope") private val

    scope: CoroutineScope ) { fun processStateCollection() { stateFlow .onEach { uiState -> searchComposablesUI?.startComposingViews(uiState) }.launchIn(scope) } ... @raulhernandezl
  127. SearchComposablesUI sets ComposeView @raulhernandezl @ActivityScope class SearchComposablesUI @Inject constructor() {

    private var composeView: ComposeView? = null // Compose content fun setComposeView( composeView: ComposeView ) { this.composeView = composeView } ...
  128. SearchComposablesUI sets content @raulhernandezl @ActivityScope class SearchComposablesUI @Inject constructor() {

    private var composeView: ComposeView? = null ... fun startComposingViews(uiState: TweetsUIState) { composeView?.setContent { TweetsWithSearchTheme { // default MaterialTheme ... } } } ... }
  129. SearchComposablesUI: Compose needs Composables @raulhernandezl @ActivityScope class SearchComposablesUI @Inject constructor()

    { private var composeView: ComposeView? = null ... fun startComposingViews(uiState: TweetsUIState) { this.composeView?.setContent { TweetsWithSearchTheme { StatesUI(uiState) } } } ... } @Composable
  130. SearchComposablesUI composes States @raulhernandezl @Composable fun StatesUI(uiState: TweetsUIState) { when

    (uiState) { is TweetsUIState.XXXXXState -> // TODO ... } }
  131. Declarative (Stateless) UI handling alternatives @raulhernandezl

  132. @raulhernandezl Declarative UI UseCase dispatches state Imperative UI View Delegate

    triggers action renders intends action Presenter executes action STATE StateFlowHandler UIStateHandler handles UI state
  133. Presenter Use Case Repository View Network data source DB data

    source View Delegate View Listener COLLECT STATEFLOW @raulhernandezl Flow Flow StateFlow results StateFlow results results StateFlow UIStateHandler
  134. TweetsListUIFragment without SearchStateHandler public class TweetsListFragmentUI extends BaseFragment { @Inject

    SearchUIStateHandler searchUIStateHandler; @Inject SearchViewDelegate viewDelegate; SearchStateHandler stateHandler; ... } @raulhernandezl
  135. TweetsListUIFragment without SearchStateHandler @raulhernandezl public class TweetsListUIFragment extends BaseFragment {

    ... public void initStateFlowAndViews() { searchUIStateHandler .setComposeView(composeView) searchUIStateHandler .initStateFlowAndViews(viewDelegate.getStateFlow()); }
  136. SearchUIStateHandler with StateFlow @raulhernandezl @ActivityScope class SearchUIStateHandler @Inject constructor() {

    private lateinit var stateFlow: StateFlow<TweetsUIState> private var composeView: ComposeView? = null fun initStateFlowAndViews(stateFlowUI: StateFlow<TweetsUIState>) { stateFlow = stateFlowUI composeView?.setContent { TweetsWithSearchTheme { StatesUI() } } } ...
  137. SearchUIStateHandler collect as State @raulhernandezl @ActivityScope class SearchUIStateHandler @Inject constructor()

    { private lateinit var stateFlow: StateFlow<TweetsUIState> ... @Composable fun StatesUI() { val state: State<TweetsUIState> = stateFlow.collectAsState() StateUIValue(state.value) } }
  138. Declarative (Stateless) UI Rendering @raulhernandezl

  139. SearchUIStateHandler composes “TweetsGrid” element @raulhernandezl @Composable fun StateUIValue(uiState: TweetsUIState) {

    when (uiState) { is TweetsUIState.ListResultsUIState -> TweetsGrid(tweets = uiState.tweets) } }
  140. “TweetsGrid” composable @raulhernandezl @Composable fun TweetsGrid(tweets: List<Tweet>) { LazyVerticalGrid(modifier =

    Modifier.fillMaxWidth(), cells = GridCells.Fixed(2) ) { items( count = tweets.size, itemContent = { index -> val tweet = tweets[index] TweetBox(tweet = tweet, onTweetClick(tweet)) } ) } } LazyVerticalGrid items count itemContent TweetBox
  141. “onTweetClick” lambda @raulhernandezl private fun onTweetClick( tweet: Tweet ): ()

    -> Unit { return { // lambda if (composeView != null) { // go to the tweet detail screen TweetDetails.navigateTo(composeView, tweet.title, tweet.id) } } } LazyVerticalGrid tweet TweetDetail
  142. “TweetBox” composable @raulhernandezl @Composable fun TweetBox( tweet: Tweet, onClick: ()

    -> Unit ) { Box(modifier = Modifier .wrapContentWidth() .clickable(onClick = onClick) // go to the tweet details screen .border(width = 1.dp,color = Color.LightGray,shape = ...), contentAlignment = Alignment.Center ) { ... } } Box (properties) Modifier clickable border wrapContentWidth content Alignment center
  143. “TweetBox” Column composable @Composable fun TweetBox(...) { Box(...) { Column(

    modifier = Modifier.padding(10.dp), verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally ) { ... } } } @raulhernandezl Box Column
  144. “TweetBox” Image composable’s remember @Composable fun TweetBox(...) { Box(...) {

    val imageUrl = tweet.images[0] Column( ...) { Image( painter = rememberCoilPainter( request = imageUrl, shouldRefetchOnSizeChange = { _, _ -> false } ), modifier = Modifier.size(60.dp) ... ) ... } } } @raulhernandezl Box Column Image
  145. “TweetBox” Image composable’s contentDescription @Composable fun TweetBox(...) { Box(...) {

    val userName = tweet.user ?: "" Column( ...) { Image( ... contentDescription = "$userName avatar image", ) ... } } } @raulhernandezl Box Column Image
  146. “TweetBox” composable’s Column elements @Composable fun TweetBox(tweet: Tweet) { ...

    Image( ... ) Spacer(modifier = Modifier.height(10.dp)) CenteredText(text = userName) } } } @raulhernandezl Box Column Image Spacer Text
  147. SearchUIStateHandler composes “CenteredText” @raulhernandezl @Composable fun StateUIValue(uiState: TweetsUIState) { when

    (uiState) { is TweetsUIState.ListResultsUIState -> TweetsList(tweets = uiState.tweets) is TweetsUIState.LoadingUIState -> CenteredText(msg = stringResource(R.string.loading_feed)) is TweetsUIState.EmptyUIState -> CenteredText(msg = stringResource(R.string.query, uiState.query)) is TweetsUIState.ErrorUIState -> CenteredText(msg = ”Error happened: ${uiState.msg}”) } }
  148. “CenteredText” composable @raulhernandezl @Composable private fun CenteredText(msg: String) { Text(

    text = msg, modifier = Modifier.padding(16.dp) .wrapContentSize(Alignment.Center), style = MaterialTheme.typography.body1, overflow = TextOverflow.Ellipsis ) } Text text modifier style overflow
  149. SearchUIStateHandler composes “TopText” @raulhernandezl @Composable fun StateUIValue(uiState: TweetsUIState) { when

    (uiState) { is TweetsUIState.ListResultsUIState -> TweetList(tweets = uiState.tweets) is TweetsUIState.LoadingUIState -> CenteredText(msg = stringResource(R.string.loading_feed)) is TweetsUIState.EmptyUIState -> CenteredText(msg = stringResource(R.string.query, uiState.query)) is TweetsUIState.ErrorUIState -> CenteredText(msg = ”Error happened: ${uiState.msg}”) is TweetsUIState.IdleUIState -> TopText() } }
  150. Cleaning up resources @raulhernandezl

  151. When using SearchStateHandler, clean up @raulhernandezl @ActivityScope class SearchStateHandler @Inject

    constructor( @Named("CoroutineUIScope") private val scope: CoroutineScope ) { private var searchComposablesUI: SearchComposablesUI? = null private var tweetsListUI: TweetsListUI? = null fun cancel() { scope.cancel() searchComposablesUI = null // Declarative tweetsListUI = null // Imperative }
  152. When using SearchUIStateHandler, clean up @raulhernandezl @ActivityScope class SearchUIStateHandler @Inject

    constructor() { private var composeView: ComposeView? = null fun destroyViews() { composeView = null // Declarative UI } }
  153. TweetsListFragmentUI clean up @raulhernandezl public class TweetsListFragmentUI extends BaseFragment {

    @Inject SearchViewDelegate viewDelegate; @Inject SearchStateHandler stateFlowHandler; @Inject SearchUIStateHandler searchUIStateHandler; @Override public void onDestroyView() { viewDelegate.cancel(); // cancels View & other Coroutines jobs stateFlowHandler.cancel(); // cancels StateFlow collection searchUIStateHandler.destroyViews(); // destroy views refs super.onDestroyView(); } }
  154. Declarative ready with Unidirectional Data Flow! @raulhernandezl

  155. Tweets Search sample app with Compose • Initial text •

    Loading text • Empty results text • List of Results • Error text @raulhernandezl
  156. Lessons learned @raulhernandezl

  157. Interoperability @raulhernandezl

  158. Recomposition runs more times than one @raulhernandezl

  159. remember would save a previous state in the View @raulhernandezl

  160. Composition Local is the “simplest” way to pass Data Flow

    to the tree and its children @raulhernandezl
  161. Composables should be Side Effects free @raulhernandezl

  162. UDF likes states @raulhernandezl

  163. Imperative vs Declarative programming @raulhernandezl

  164. Why Compose? @raulhernandezl

  165. Pros & Cons Compose Parallel order & Frequently (classic) Views

    Sequential order Compose Stateless = Immutable = Deterministic (classic) Views Stateful = Mutable = Unpredictable Compose Kotlin (classic) Views Java / Kotlin @raulhernandezl
  166. Performance improvements @raulhernandezl

  167. Testability @raulhernandezl

  168. Reusability of UI components @raulhernandezl

  169. Experimentation of new ways of working @raulhernandezl

  170. Next Steps @raulhernandezl

  171. Start adopting explicit state declarations TODAY @raulhernandezl

  172. References @raulhernandezl Getting ready for Declarative UIs series • Part

    1 Unidirectional Data Flow • Part 2 Implementing Unidirectional Data Flow • Part 3 Why Declarative UIs on Android? • Synchronous communication with the UI using StateFlow
  173. Special thanks @raulhernandezl Nick Butcher

  174. Thank you. @raulhernandezl

  175. Questions? @raulhernandezl

  176. (Used) Dependencies note: this may change really quickly, read official

    sources to verify @raulhernandezl
  177. Gradle dependencies for Kotlin Coroutines @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.2' kotlin_stdlib_version = '1.4.32'
  178. Gradle’s app dependencies for Jetpack Compose @raulhernandezl app/build.gradle ... implementation

    "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.runtime:runtime:$compose_version" implementation "androidx.compose.foundation:foundation-layout:$compose_version" implementation "androidx.compose.ui:ui-tooling:$compose_version" implementation "com.google.accompanist:accompanist-coil:$accomp_coil_version" compose_version = '1.0.0-beta05' accomp_coil_version = '0.8.1'
  179. Gradle’s project dependencies for Jetpack Compose @raulhernandezl build.gradle … dependencies

    { classpath 'com.android.tools.build:gradle:7.0.0-alpha14' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32' }
  180. Gradle’s wrapper dependencies for Jetpack Compose @raulhernandezl gradle/wrapper/gradle-wrapper.properties ... distributionUrl=https://services.gradle.org/distributions/gradle-7.0-rc-1-bin

    .zip