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

Getting ready for Declarative UIs with Unidirec...

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)
- Brighton Kotlin (June the 24th 2021)
- Kotlin Stuttgart (KUG) Vol. 9 (July the 14th 2021)

Raul Hernandez Lopez

February 07, 2021
Tweet

More Decks by Raul Hernandez Lopez

Other Decks in Programming

Transcript

  1. 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
  2. Tweets Search sample app @raulhernandezl • Loading spinner • Empty

    results text • List of results • Error message text
  3. Repository Network data source DB data source Data Layer @raulhernandezl

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

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

    @raulhernandezl requests executes callback (results) results callback (results) • Presenter connects business logic with views
  6. 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
  7. Presenter Use Case Repository View / Callbacks Network data source

    DB data source View Delegate View Listener DATA SOURCES: SUSPEND @raulhernandezl results results
  8. 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
  9. 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
  10. Presenter Use Case Repository View / Callbacks Network data source

    DB data source CALLBACKS or NOT? @raulhernandezl results results results callback (results) callback (results)
  11. 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
  12. Lack of a return value. Results are declared as input

    parameters. @raulhernandezl Consequences of RT broken?
  13. 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
  14. 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
  15. Presenter Use Case Repository View / Callbacks Network data source

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

    val connectionHandler: ConnectionHandler, private val requestsIOHandler: RequestsIOHandler ) : NetworkDataSource NetworkDataSourceImpl constructor dependencies @raulhernandezl
  17. @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
  18. 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
  19. @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
  20. @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
  21. 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? ???
  22. 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!
  23. @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
  24. 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!
  25. 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
  26. Presenter Use Case Repository View / Callbacks Network data source

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

    private val scope: CoroutineScope ) : UseCase<SearchCallback> { SearchTweetUseCase: constructor dependencies @raulhernandezl
  28. @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())
  29. UseCase contract w/ callback interface UseCase<T> { fun execute(param: String,

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

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

    callback?.onShowLoader() scope.launch { ... } } SearchTweetUseCase: Kotlin Flow w/ scope.launch @raulhernandezl
  32. 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
  33. 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
  34. 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
  35. override fun cancel() { callback = null scope.cancel() } SearchTweetUseCase:

    cancellation with Structured concurrency @raulhernandezl private val scope = CoroutineScope( taskThreading.uiDispatcher() + SupervisorJob())
  36. 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
  37. @RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, @Named("CoroutineRetainedScope")

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

    String, callbackInput: SearchCallback?) fun cancel() fun getStateFlow(): StateFlow<T> }
  39. 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
  40. @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
  41. @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
  42. /** * UI States defined for StateFlow in the workflow

    */ sealed class TweetsUIState { data class ListResultsUIState(val tweets: List<Tweet>): TweetsUIState() } TweetsUIState w/ Results state @raulhernandezl
  43. 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
  44. 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() }
  45. 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() }
  46. 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() }
  47. 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() }
  48. override fun execute(query: String) { callback?.onShowLoader() repository.searchTweet(query) .onStart { tweetsStateFlow.value

    = TweetsUIState.LoadingUIState } SearchTweetUseCase: Loading state propagation @raulhernandezl
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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(); }
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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 } ... }
  59. SearchStateHandler collects StateFlow w/ onEach private lateinit var stateFlow: StateFlow<TweetsUIState>

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

    stateFlow .onEach { uiState -> tweetsListUI?.handleStates(uiState) }.launchIn(scope) } @raulhernandezl
  61. @raulhernandezl Imperative UI StateFlowHandler UseCase communicates state dispatches state Imperative

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

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

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

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

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

    { hideLoader() hideError() showList() updateList(tweets) } SearchStateHandler Adapter ViewHolder tweets action StateFlow TweetsUIState.ListUIState
  67. 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) } }
  68. 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 -> {} } }
  69. @raulhernandezl Imperative Declarative UI StateFlowHandler UseCase dispatches collected state dispatches

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

    SearchComposablesUI searchComposablesUI; @Inject SearchViewDelegate viewDelegate; @Inject SearchStateHandler stateHandler; ... } @raulhernandezl
  71. 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
  72. 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
  73. 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
  74. 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 }
  75. TweetsListUIFragment delegates handling to SearchStateHandler @raulhernandezl public class TweetsListUIFragment extends

    BaseFragment { ... public void initStateFlowAndViews() { stateHandler.initStateFlowAndViews(viewDelegate.getStateFlow(), searchComposablesUI); stateHandler.processStateFlowCollection(); }
  76. 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 } ... }
  77. TweetsListUIFragment delegates to SearchStateHandler @raulhernandezl public class TweetsListUIFragment extends BaseFragment

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

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

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

    private var composeView: ComposeView? = null ... fun startComposingViews(uiState: TweetsUIState) { composeView?.setContent { TweetsWithSearchTheme { // default MaterialTheme ... } } } ... }
  81. 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
  82. @raulhernandezl Declarative UI UseCase dispatches state Imperative UI View Delegate

    triggers action renders intends action Presenter executes action STATE StateFlowHandler UIStateHandler handles UI state
  83. 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
  84. TweetsListUIFragment without SearchStateHandler public class TweetsListFragmentUI extends BaseFragment { @Inject

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

    ... public void initStateFlowAndViews() { searchUIStateHandler .setComposeView(composeView) searchUIStateHandler .initStateFlowAndViews(viewDelegate.getStateFlow()); }
  86. 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() } } } ...
  87. 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) } }
  88. SearchUIStateHandler composes “TweetsGrid” element @raulhernandezl @Composable fun StateUIValue(uiState: TweetsUIState) {

    when (uiState) { is TweetsUIState.ListResultsUIState -> TweetsGrid(tweets = uiState.tweets) } }
  89. “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
  90. “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
  91. “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
  92. “TweetBox” Column composable @Composable fun TweetBox(...) { Box(...) { Column(

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

    val imageUrl = tweet.images[0] Column( ...) { Image( painter = rememberImagePainter( data = imageUrl, onExecute = { _, _ -> true } ), builder = { placeholder(R.drawable.view_holder) } modifier = Modifier.size(60.dp) ... ) ... } } } @raulhernandezl Box Column Image
  94. “TweetBox” Image composable’s contentDescription @Composable fun TweetBox(...) { Box(...) {

    val userName = tweet.user ?: "" Column( ...) { Image( ... contentDescription = "$userName avatar image", ) ... } } } @raulhernandezl Box Column Image
  95. “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
  96. 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}”) } }
  97. “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
  98. 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() } }
  99. 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 }
  100. When using SearchUIStateHandler, clean up @raulhernandezl @ActivityScope class SearchUIStateHandler @Inject

    constructor() { private var composeView: ComposeView? = null fun destroyViews() { composeView = null // Declarative UI } }
  101. 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(); } }
  102. Tweets Search sample app with Compose • Initial text •

    Loading text • Empty results text • List of Results • Error text @raulhernandezl
  103. Composition Local is the “simplest” way to pass Data Flow

    to the tree and its children @raulhernandezl
  104. 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
  105. 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
  106. 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.5.0' kotlin_stdlib_version = '1.5.10'
  107. 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 "io.coil-kt:coil-compose:$coil_compose" compose_version = '1.0.0-rc02' coil_compose = '1.3.0'
  108. Gradle’s project dependencies for Jetpack Compose @raulhernandezl build.gradle … dependencies

    { classpath 'com.android.tools.build:gradle:7.1.0-alpha03' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10' }