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.

Ffc500baeba9a1024e2c8273203c9f90?s=128

Raul Hernandez Lopez

February 07, 2021
Tweet

Transcript

  1. None
  2. None
  3. None
  4. None
  5. None
  6. • • • •

  7. None
  8. None
  9. Repository Network data source DB data source •

  10. Use Case Repository •

  11. Presenter Use Case •

  12. Presenter View •

  13. View •

  14. Presenter View View Delegate View Listener • •

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

    DB data source View Delegate View Listener
  17. Presenter Use Case Repository View / Callbacks Network data source

    View Delegate View Listener Flow DB data source
  18. Presenter Use Case Repository View / Callbacks Network data source

    DB data source View Delegate View Listener Flow
  19. Presenter Use Case Repository View / Callbacks Network data source

    DB data source View Delegate View Listener Channels as Flows
  20. None
  21. Presenter Use Case Repository View / Callbacks Network data source

    DB data source
  22. None
  23. None
  24. None
  25. None
  26. None
  27. None
  28. None
  29. None
  30. None
  31. None
  32. None
  33. None
  34. None
  35. Presenter Use Case Repository View Network data source DB data

    source View Delegate View Listener Flow StateFlow StateFlow StateFlow
  36. Presenter Use Case Repository View Network data source DB data

    source View Delegate View Listener Channels as Flows Flow Flow StateFlow StateFlow Handler StateFlow StateFlow
  37. None
  38. None
  39. UI State Action

  40. UseCase Action Presenter Imperative UI View Delegate

  41. Imperative UI StateFlowHandler UseCase Imperative UI State View Delegate Presenter

  42. None
  43. Imperative Declarative UI StateFlowHandler UseCase Imperative UI View Delegate Presenter

    State
  44. None
  45. None
  46. None
  47. None
  48. None
  49. Presenter Use Case Repository View / Callbacks Network data source

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

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

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

    DESC") suspend fun retrieveAllTweetsRxForTweetsIds(tweetIds: List<String>): List<Tweet>
  53. None
  54. Presenter Use Case Repository View / Callbacks Network data source

    View Delegate View Listener Flow 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 {
  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>>
  57. suspend fun getSearchTweets(query: String): List<Tweet> { ... val tweetIds =

    tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) return tweetsDataSource.retrieveAllTweetsForTweetsIdsFlow(tweetIds) }
  58. suspend fun getSearchTweets(query: String): List<Tweet> { ... val tweetIds =

    tweetQueryJoinDataSource.retrieveAllTweetsIdAQuery(query) return tweetsDataSource.retrieveAllTweetsForTweetsIdsFlow(tweetIds) }
  59. suspend fun getSearchTweets(query: String): List<Tweet> { ... return tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds) }

  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>
  61. None
  62. fun getSearchTweets(query: String): Flow<List<Tweet>> = flow { ... emitAll(tweetsDataSource.retrieveAllTweetsForTweetsIdsFlow(tweetIds)) }.flowOn(taskThreading.ioDispatcher())

  63. fun getSearchTweets(query: String): Flow<List<Tweet>> = flow { ... emit(tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds)) }.flowOn(taskThreading.ioDispatcher())

  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())
  65. None
  66. Presenter Use Case Repository View / Callbacks Network data source

    DB data source View Delegate View Listener Flow
  67. Presenter Use Case Repository View / Callbacks Network data source

    DB data source
  68. @RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, @Named("CoroutineRetainedScope")

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

    private val scope: CoroutineScope ) : UseCase<SearchCallback> { private val scope = CoroutineScope( taskThreading.uiDispatcher() + SupervisorJob())
  70. None
  71. None
  72. SCOPE + SupervisorJob JOB 3 JOB 2 JOB 1

  73. None
  74. interface UseCase<T> { fun execute(param: String, callbackInput: T?) fun cancel()

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

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

    callback?.onShowLoader() scope.launch { ... } }
  77. override fun execute(query: String, callbackInput: SearchCallback?) { callback = callbackInput

    callback?.onShowLoader() scope.launch { repository.searchTweet(query) .map { // some computation } .flowOn(taskThreading.computationDispatcher()) ... } }
  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) } } }
  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) }
  80. override fun cancel() { callback = null scope.cancel() } private

    val scope = CoroutineScope( taskThreading.uiDispatcher() + SupervisorJob())
  81. None
  82. Presenter Use Case Repository View / Callbacks Network data source

    DB data source View Delegate View Listener Channels as Flows
  83. @ActivityScope class SearchViewDelegate constructor( private val presenter: SearchTweetPresenter, private val

    taskThreading: TaskThreading, @Named("CoroutineUIScope") private val scope: CoroutineScope ) { @ExperimentalCoroutinesApi fun prepareViewDelegateListener(val view: SearchView): Flow<String> = (channelFlow<String> { // define listener val listener = SearchViewListener(channel) ... }).flowOn(taskThreading.ioDispatcher()) }
  84. @ExperimentalCoroutinesApi fun prepareViewDelegateListener(val view: SearchView): Flow<String> = (channelFlow<String> { val

    listener = SearchViewListener(channel) view.setOnQueryTextListener(listener) awaitClose { view.setOnQueryTextListener(null) } }).flowOn(taskThreading.ioDispatcher())
  85. class SearchViewListener constructor( private val queryChannel: SendChannel<String> ): SearchView.OnQueryTextListener {

    override fun onQueryTextChange(query: String): Boolean { ... return true } override fun onQueryTextSubmit(query: String) = false }
  86. 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 }
  87. @FlowPreview fun observeChannelAsFlow() { declareViewDelegate() .debounce(600) .distinctUntilChanged() .filter { query

    -> filterQuery(query) } .flowOn(taskThreading.computationDispatcher()) ... }
  88. @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() }
  89. None
  90. Presenter Use Case Repository View Network data source DB data

    source View Delegate View Listener Flow StateFlow StateFlow StateFlow
  91. @RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, @Named("CoroutineRetainedScope")

    private val scope: CoroutineScope ) : UseCase<SearchCallback> { ) : UseCaseFlow<?> {
  92. interface UseCaseFlow<T> { fun execute(param: String, callbackInput: SearchCallback?) fun cancel()

    fun getStateFlow(): StateFlow<T> }
  93. interface UseCaseFlow<T> { fun execute(param: String, callbackInput: SearchCallback?) fun cancel()

    fun getStateFlow(): StateFlow<T> }
  94. @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
  95. @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
  96. /** * UI States defined for StateFlow in the workflow

    */ sealed class TweetsUIState { data class ListResultsUIState(val tweets: List<Tweet>): TweetsUIState() }
  97. /** * UI States defined for StateFlow in the workflow

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

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

    */ sealed class TweetsUIState { @Immutable data class ListResultsUIState(val tweets: List<Tweet>): TweetsUIState() @Immutable data class ErrorUIState(val msg: String): TweetsUIState() @Immutable data class EmptyUIState(val query: String): TweetsUIState() }
  100. /** * UI States defined for StateFlow in the workflow

    */ sealed class TweetsUIState { @Immutable data class ListResultsUIState(val tweets: List<Tweet>): TweetsUIState() @Immutable data class ErrorUIState(val msg: String): TweetsUIState() @Immutable data class EmptyUIState(val query: String): TweetsUIState() object LoadingUIState: TweetsUIState() }
  101. /** * UI States defined for StateFlow in the workflow

    */ sealed class TweetsUIState { @Immutable data class ListResultsUIState(val tweets: List<Tweet>): TweetsUIState() @Immutable data class ErrorUIState(val msg: String): TweetsUIState() @Immutable data class EmptyUIState(val query: String): TweetsUIState() object LoadingUIState: TweetsUIState() object IdleUIState: TweetsUIState() }
  102. override fun execute(query: String) { callback?.onShowLoader() repository.searchTweet(query) .onStart { tweetsStateFlow.value

    = TweetsUIState.LoadingUIState }
  103. 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) }
  104. 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) }
  105. 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)
  106. Presenter Use Case Repository View Network data source DB data

    source View Delegate View Listener Flow Flow StateFlow StateFlow Handler StateFlow StateFlow
  107. @ActivityScope public class SearchTweetPresenter { @NotNull private final SearchTweetUseCase tweetSearchUseCase;

    @Inject public SearchTweetPresenter( @NotNull SearchTweetUseCase tweetSearchUseCase ) { this.tweetSearchUseCase = tweetSearchUseCase; } public void cancel() { tweetSearchUseCase.cancel(); }
  108. @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(); }
  109. @ActivityScope class SearchViewDelegate @Inject constructor( private val presenter: SearchTweetPresenter, private

    val taskThreading: TaskThreading @Named("CoroutineUIScope") private val scope: CoroutineScope ) { fun getStateFlow(): StateFlow<TweetsUIState> = presenter.stateFlow
  110. @ActivityScope class SearchViewDelegate @Inject constructor( private val presenter: SearchTweetPresenter, private

    val taskThreading: TaskThreading @Named("CoroutineUIScope") private val scope: CoroutineScope ) { fun getStateFlow(): StateFlow<TweetsUIState> = presenter.stateFlow fun initialiseProcessingQuery( searchView: SearchView, tweetsListUI: TweetsListFragmentUI? ) { tweetsListUI?.apply { initStateFlowAndViews() } ... }
  111. public class TweetsListFragmentUI extends BaseFragment { ... @Inject SearchViewDelegate viewDelegate;

    @Inject SearchStateHandler stateHandler; public void initStateFlowAndViews() { stateHandler.initStateFlowAndViews(viewDelegate.getStateFlow(), this); stateHandler.processStateFlowCollection(); }
  112. @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 } ... }
  113. @Override public View onCreateView( @NotNull LayoutInflater inflater, @Nullable ViewGroup container,

    @Nullable Bundle savedInstanceState ) { final View view = inflater.inflate(R.layout.fragment_search, container, false); viewDelegate.initialiseProcessingQuery(searchView) initStateFlowAndViews() return view; }
  114. private lateinit var stateFlow: StateFlow<TweetsUIState> fun processStateCollection() { stateFlow .onEach

    { uiState -> tweetsListUI?.handleStates(uiState) }... }
  115. fun processStateCollection() { stateFlow .onEach { uiState -> tweetsListUI?.handleStates(uiState) }.launchIn(scope)

    }
  116. None
  117. Imperative UI StateFlowHandler UseCase Imperative UI State View Delegate Presenter

  118. fun TweetsListFragmentUI.handleStates(uiState: TweetsUIState) { when (uiState) { is TweetsUIState.ListResultsUIState ->

    showResults(uiState.tweets) } }
  119. fun TweetsListFragmentUI.showResults( tweets: List<Tweet> ) { hideLoader() hideError() showList() updateList(tweets)

    }
  120. fun TweetsListFragmentUI.showResults( tweets: List<Tweet> ) { hideLoader() hideError() showList() updateList(tweets)

    }
  121. fun TweetsListFragmentUI.showResults( tweets: List<Tweet> ) { hideLoader() hideError() showList() updateList(tweets)

    } RecyclerView LayoutManager Item Decorator RecyclerView
  122. fun TweetsListFragmentUI.showResults( tweets: List<Tweet> ) { hideLoader() hideError() showList() updateList(tweets)

    } SearchStateHandler Adapter ViewHolder StateFlow TweetsUIState.ListUIState
  123. 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) } }
  124. 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 -> {} } }
  125. None
  126. Imperative Declarative UI StateFlowHandler UseCase Imperative UI State View Delegate

    Presenter
  127. public class TweetsListFragmentUI extends BaseFragment { @Inject SearchComposablesUI searchComposablesUI; @Inject

    SearchViewDelegate viewDelegate; @Inject SearchStateHandler stateHandler; ... }
  128. <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>
  129. 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; }
  130. @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, activity); ... return view; }
  131. @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, activity); return view; } fun setComposeView( composeView: ComposeView, activity: SearchTweetActivity ) { this.composeView = composeView this.activity = activity }
  132. public class TweetsListUIFragment extends BaseFragment { ... public void initStateFlowAndViews()

    { stateHandler.initStateFlowAndViews(viewDelegate.getStateFlow(), searchComposablesUI); stateHandler.processStateFlowCollection(); }
  133. @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 } ... }
  134. public class TweetsListUIFragment extends BaseFragment { ... private void initStateFlowAndViews()

    { stateHandler.initStateFlowAndViews( viewDelegate.getStateFlow(),searchComposablesUI); stateHandler.processStateFlowCollection(); }
  135. class SearchStateHandler @Inject constructor( @Named("CoroutineUIScope") private val scope: CoroutineScope )

    { fun processStateCollection() { stateFlow .onEach { uiState -> searchComposablesUI?.startComposingViews(uiState) }.launchIn(scope) } ...
  136. @ActivityScope class SearchComposablesUI @Inject constructor() { private var composeView: ComposeView?

    = null // Compose content private var activity: SearchTweetActivity? = null // Click listener ...
  137. @ActivityScope class SearchComposablesUI @Inject constructor() { private var composeView: ComposeView?

    = null private var activity: SearchTweetActivity? = null fun setComposeView( composeView: ComposeView, activity: SearchTweetActivity ) { this.composeView = composeView this.activity = activity } ...
  138. @ActivityScope class SearchComposablesUI @Inject constructor() { private var composeView: ComposeView?

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

    = null ... fun startComposingViews(uiState: TweetsUIState) { this.composeView?.setContent { TweetsWithSearchTheme { StatesUI(uiState) } } } ... } @Composable
  140. @Composable fun StatesUI(uiState: TweetsUIState) { when (uiState) { is TweetsUIState.XXXXXState

    -> // TODO ... } }
  141. None
  142. Declarative UI UseCase Imperative UI State View Delegate Presenter StateFlowHandler

    UIStateHandler
  143. Presenter Use Case Repository View Network data source DB data

    source View Delegate View Listener Flow Flow StateFlow StateFlow StateFlow UIStateHandler
  144. public class TweetsListFragmentUI extends BaseFragment { @Inject SearchUIStateHandler searchUIStateHandler; @Inject

    SearchViewDelegate viewDelegate; SearchStateHandler stateHandler; ... }
  145. public class TweetsListUIFragment extends BaseFragment { ... public void initStateFlowAndViews()

    { searchUIStateHandler .initStateFlowAndViews(viewDelegate.getStateFlow()); }
  146. @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() } } } ...
  147. @ActivityScope class SearchUIStateHandler @Inject constructor() { private lateinit var stateFlow:

    StateFlow<TweetsUIState> ... @ExperimentalCoroutinesApi @Composable fun StatesUI() { val state: State<TweetsUIState> = stateFlow.collectAsState() StateUIValue(state.value) } }
  148. None
  149. @Composable fun StateUIValue(uiState: TweetsUIState) { when (uiState) { is TweetsUIState.ListResultsUIState

    -> TweetsList(tweets = uiState.tweets) } }
  150. @Preview @Composable private fun TweetsList(tweets: List<Tweet>) { LazyColumn { items(

    count = tweets.size, itemContent = { index -> TweetRow(tweet = tweets[index]) Divider(color = Color.LightGray) } ) } } LazyColumn items count itemContent TweetRow Divider
  151. @Preview @Composable private fun TweetRow(tweet: Tweet) { Row(modifier = Modifier

    .fillMaxWidth() .clickable(onClick = { // go to the detail screen if(activity != null) { TweetDetailsActivity.navigateToDetailsActivity( activity, composeView, tweet.title, tweet.id) } }) .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { ... } } Row (properties) Modifier clickable padding fillMaxWidth vertical Alignment center
  152. @Preview @Composable private fun TweetRow(tweet: Tweet) { Row(...) { val

    userName = tweet.userName ?: "" CoilImage( data = tweet.images[0], contentDescription = "$userName image", modifier = Modifier.preferredSize(60.dp)) Spacer(modifier = Modifier.preferredSize(12.dp)) Column { Text(text = userName) } } } Row CoilImage Spacer Column Text
  153. @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”) } }
  154. @Preview @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
  155. @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() } }
  156. None
  157. @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 }
  158. @ActivityScope class SearchUIStateHandler @Inject constructor() { private var composeView: ComposeView?

    = null private var activity: SearchTweetActivity? = null fun destroyViews() { composeView = null // Declarative UI activity = null // ref to the activity for listener }
  159. public class TweetsListFragmentUI extends BaseFragment { @Inject SearchViewDelegate viewDelegate; @Inject

    SearchStateHandler stateFlowHandler; @Inject SearchUIStateHandler searchUIStateHandler; @Override public void onDestroyView() { viewDelegate.cancel(); // cancels View & other Coroutines stateFlowHandler.cancel(); // cancels StateFlow collection searchUIStateHandler.destroyViews(); // destroy views refs super.onDestroyView(); } }
  160. None
  161. • • • • •

  162. None
  163. None
  164. None
  165. None
  166. None
  167. None
  168. None
  169. None
  170. None
  171. None
  172. None
  173. None
  174. None
  175. None
  176. None
  177. None
  178. None
  179. None
  180. None
  181. 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.21-2'
  182. 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 "dev.chrisbanes.accompanist:accompanist-coil:$accomp_coil_version" compose_version = '1.0.0-alpha11' accomp_coil_version = '0.5.0'
  183. app/build.gradle ... buildFeatures { compose true } composeOptions { kotlinCompilerVersion

    "1.4.21-2" kotlinCompilerExtensionVersion compose_version }
  184. build.gradle … dependencies { classpath 'com.android.tools.build:gradle:7.0.0-alpha05' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21-2' }

  185. gradle/wrapper/gradle-wrapper.properties ... distributionUrl=https://services.gradle.org/distributions/gradle-6.8-bin.zip