Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Software Engineer Raul Hernandez Lopez @ Twitter raulh82vlc @raulhernandezl

Slide 3

Slide 3 text

Agenda @raulhernandezl

Slide 4

Slide 4 text

1. Use case 2. Analyse an existing architecture 3. Adopting Unidirectional Data Flow 4. Implementation 5. Lessons learned 6. Why Compose? 7. Next steps @raulhernandezl AGENDA

Slide 5

Slide 5 text

Use case @raulhernandezl

Slide 6

Slide 6 text

Tweets Search sample app @raulhernandezl ● Loading spinner ● Empty results text ● List of results ● Error message text

Slide 7

Slide 7 text

Analyse an existing architecture @raulhernandezl

Slide 8

Slide 8 text

Start adopting on an existing Architecture @raulhernandezl

Slide 9

Slide 9 text

Repository Network data source DB data source Data Layer @raulhernandezl results results results ● Repository manages data sources

Slide 10

Slide 10 text

Use Case Repository Business Layer @raulhernandezl requests results ● Use Case performs any business logic and returns values callback (results) transforms

Slide 11

Slide 11 text

Presenter Use Case Model View Presenter (MVP) + Clean Architecture @raulhernandezl requests executes callback (results) results callback (results) ● Presenter connects business logic with views

Slide 12

Slide 12 text

Presenter View View connection with Presenter @raulhernandezl executes callback (results) callback (results) ● View receives new data

Slide 13

Slide 13 text

View User intention @raulhernandezl ● The user starts typing a query of interest types a new query

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Analysing Coroutines @raulhernandezl

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

How to get ready for Declarative UIs? @raulhernandezl

Slide 20

Slide 20 text

Presenter Use Case Repository View / Callbacks Network data source DB data source CALLBACKS or NOT? @raulhernandezl results results results callback (results) callback (results)

Slide 21

Slide 21 text

Any disadvantages on Callbacks? @raulhernandezl

Slide 22

Slide 22 text

Lack of readability = Callback hell @raulhernandezl Callbacks into callbacks

Slide 23

Slide 23 text

Callbacks break Referential Transparency principle @raulhernandezl

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Consequences of RT broken? @raulhernandezl

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Lack of a return value. Results are declared as input parameters. @raulhernandezl Consequences of RT broken?

Slide 28

Slide 28 text

Results can mutate. @raulhernandezl

Slide 29

Slide 29 text

Lack of Determinism Results can mutate. @raulhernandezl

Slide 30

Slide 30 text

Are there better alternatives to Callbacks? @raulhernandezl

Slide 31

Slide 31 text

StateFlow Always returns last value when different. @raulhernandezl

Slide 32

Slide 32 text

StateFlow Results are immutable. @raulhernandezl

Slide 33

Slide 33 text

StateFlow is Deterministic by default. @raulhernandezl

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Adopting the Unidirectional Data Flow (UDF) @raulhernandezl

Slide 37

Slide 37 text

One-way data flow @raulhernandezl

Slide 38

Slide 38 text

@raulhernandezl UI State Action transforms results renders intends

Slide 39

Slide 39 text

@raulhernandezl UseCase triggers action Presenter executes action Imperative UI intends action View Delegate ACTION

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Imperative & Declarative UIs can co-exist @raulhernandezl

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Advantages @raulhernandezl

Slide 45

Slide 45 text

Flexibility @raulhernandezl

Slide 46

Slide 46 text

Layers composition @raulhernandezl

Slide 47

Slide 47 text

Implementation @raulhernandezl

Slide 48

Slide 48 text

Data Sources: Network & DB @raulhernandezl

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Repository: suspend & Flow @raulhernandezl

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

suspend fun getSearchTweets(query: String): List { ... return tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds) } Case 1) TweetsRepository: a suspend function is needed @raulhernandezl YES!

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Case 2) TweetsRepository: Flow @raulhernandezl

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

fun getSearchTweets(query: String): Flow> = flow { ... // retrieve old values from DB emit(tweetsDataSource.retrieveAllTweetsForTweetsIds(tweetIds)) // get fresh values from network & saved them into DB ... // 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!

Slide 65

Slide 65 text

Use Case Overview @raulhernandezl

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Presenter Use Case Repository View / Callbacks Network data source DB data source WHERE ARE CALLBACKS? @raulhernandezl results results results callback (results) callback (results) transforms

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

@RetainedScope class SearchTweetUseCase @Inject constructor( private val tweetsRepository: TweetsRepository, @Named("CoroutineRetainedScope") private val scope: CoroutineScope ) : UseCase { SearchTweetUseCase: constructor dependencies @raulhernandezl private val scope = CoroutineScope( taskThreading.uiDispatcher() + SupervisorJob())

Slide 70

Slide 70 text

AVOID SIDE-EFFECTS ON CANCELLATION SCOPE + SupervisorJob @raulhernandezl

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

Use Case Implementation @raulhernandezl

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

Synchronous communication with the UI @raulhernandezl

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

SearchViewDelegate gets StateFlow from Presenter @raulhernandezl @ActivityScope class SearchViewDelegate @Inject constructor( private val presenter: SearchTweetPresenter @Named("CoroutineUIScope") private val scope: CoroutineScope ) { fun getStateFlow(): StateFlow = presenter.stateFlow

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

StateFlowHandler: constructor @raulhernandezl @ActivityScope class SearchStateHandler @Inject constructor( @Named("CoroutineUIScope") private val scope: CoroutineScope ) { private var tweetsListUI: TweetsListUIFragment? = null private lateinit var stateFlow: StateFlow fun initStateFlowAndViews( val stateFlow: StateFlow, val tweetsListUI: TweetsListUIFragment ) { this.stateFlow = stateFlow this.tweetsListUI = tweetsListUI } ... }

Slide 105

Slide 105 text

SearchStateHandler collects StateFlow w/ onEach private lateinit var stateFlow: StateFlow fun processStateCollection() { stateFlow .onEach { uiState -> tweetsListUI?.handleStates(uiState) }... } @raulhernandezl

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

Imperative (Stateful) UI Rendering @raulhernandezl

Slide 108

Slide 108 text

@raulhernandezl Imperative UI StateFlowHandler UseCase communicates state dispatches state Imperative UI View Delegate triggers action renders intends action Presenter executes action STATE

Slide 109

Slide 109 text

TweetsListUIFragment handles State to show Tweets @raulhernandezl fun TweetsListFragmentUI.handleStates(uiState: TweetsUIState) { when (uiState) { is TweetsUIState.ListResultsUIState -> showResults(uiState.tweets) } }

Slide 110

Slide 110 text

TweetsListUIFragment handles Stateful Loader @raulhernandezl fun TweetsListFragmentUI.showResults( tweets: List ) { hideLoader() hideError() showList() updateList(tweets) } Loading data... Loading data...

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

TweetsListUIFragment handles Stateful RecyclerView @raulhernandezl fun TweetsListFragmentUI.showResults( tweets: List ) { hideLoader() hideError() showList() updateList(tweets) } SearchStateHandler Adapter ViewHolder tweets action StateFlow TweetsUIState.ListUIState

Slide 114

Slide 114 text

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) } }

Slide 115

Slide 115 text

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 -> {} } }

Slide 116

Slide 116 text

Declarative (Stateless) UI migration @raulhernandezl

Slide 117

Slide 117 text

@raulhernandezl Imperative Declarative UI StateFlowHandler UseCase dispatches collected state dispatches state Imperative UI View Delegate triggers action renders intends action Presenter executes action STATE

Slide 118

Slide 118 text

TweetsListUIFragment: Compose Interoperability public class TweetsListFragmentUI extends BaseFragment { @Inject SearchComposablesUI searchComposablesUI; @Inject SearchViewDelegate viewDelegate; @Inject SearchStateHandler stateHandler; ... } @raulhernandezl

Slide 119

Slide 119 text

XML layout Interoperability @raulhernandezl

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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 }

Slide 123

Slide 123 text

TweetsListUIFragment delegates handling to SearchStateHandler @raulhernandezl public class TweetsListUIFragment extends BaseFragment { ... public void initStateFlowAndViews() { stateHandler.initStateFlowAndViews(viewDelegate.getStateFlow(), searchComposablesUI); stateHandler.processStateFlowCollection(); }

Slide 124

Slide 124 text

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 fun initStateFlowAndViews( val stateFlow: StateFlow, val searchComposablesUI: SearchComposablesUI ) { this.stateFlow = stateFlow this.searchComposablesUI = searchComposablesUI } ... }

Slide 125

Slide 125 text

TweetsListUIFragment delegates to SearchStateHandler @raulhernandezl public class TweetsListUIFragment extends BaseFragment { ... private void initStateFlowAndViews() { stateHandler.initStateFlowAndViews( viewDelegate.getStateFlow(),searchComposablesUI); stateHandler.processStateFlowCollection(); }

Slide 126

Slide 126 text

SearchStateHandler starts SearchComposablesUI class SearchStateHandler @Inject constructor( @Named("CoroutineUIScope") private val scope: CoroutineScope ) { fun processStateCollection() { stateFlow .onEach { uiState -> searchComposablesUI?.startComposingViews(uiState) }.launchIn(scope) } ... @raulhernandezl

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

SearchComposablesUI composes States @raulhernandezl @Composable fun StatesUI(uiState: TweetsUIState) { when (uiState) { is TweetsUIState.XXXXXState -> // TODO ... } }

Slide 131

Slide 131 text

Declarative (Stateless) UI handling alternatives @raulhernandezl

Slide 132

Slide 132 text

@raulhernandezl Declarative UI UseCase dispatches state Imperative UI View Delegate triggers action renders intends action Presenter executes action STATE StateFlowHandler UIStateHandler handles UI state

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

TweetsListUIFragment without SearchStateHandler @raulhernandezl public class TweetsListUIFragment extends BaseFragment { ... public void initStateFlowAndViews() { searchUIStateHandler .setComposeView(composeView) searchUIStateHandler .initStateFlowAndViews(viewDelegate.getStateFlow()); }

Slide 136

Slide 136 text

SearchUIStateHandler with StateFlow @raulhernandezl @ActivityScope class SearchUIStateHandler @Inject constructor() { private lateinit var stateFlow: StateFlow private var composeView: ComposeView? = null fun initStateFlowAndViews(stateFlowUI: StateFlow) { stateFlow = stateFlowUI composeView?.setContent { TweetsWithSearchTheme { StatesUI() } } } ...

Slide 137

Slide 137 text

SearchUIStateHandler collect as State @raulhernandezl @ActivityScope class SearchUIStateHandler @Inject constructor() { private lateinit var stateFlow: StateFlow ... @Composable fun StatesUI() { val state: State = stateFlow.collectAsState() StateUIValue(state.value) } }

Slide 138

Slide 138 text

Declarative (Stateless) UI Rendering @raulhernandezl

Slide 139

Slide 139 text

SearchUIStateHandler composes “TweetsGrid” element @raulhernandezl @Composable fun StateUIValue(uiState: TweetsUIState) { when (uiState) { is TweetsUIState.ListResultsUIState -> TweetsGrid(tweets = uiState.tweets) } }

Slide 140

Slide 140 text

“TweetsGrid” composable @raulhernandezl @Composable fun TweetsGrid(tweets: List) { 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

Slide 141

Slide 141 text

“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

Slide 142

Slide 142 text

“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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

“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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

“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

Slide 147

Slide 147 text

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}”) } }

Slide 148

Slide 148 text

“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

Slide 149

Slide 149 text

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() } }

Slide 150

Slide 150 text

Cleaning up resources @raulhernandezl

Slide 151

Slide 151 text

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 }

Slide 152

Slide 152 text

When using SearchUIStateHandler, clean up @raulhernandezl @ActivityScope class SearchUIStateHandler @Inject constructor() { private var composeView: ComposeView? = null fun destroyViews() { composeView = null // Declarative UI } }

Slide 153

Slide 153 text

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(); } }

Slide 154

Slide 154 text

Declarative ready with Unidirectional Data Flow! @raulhernandezl

Slide 155

Slide 155 text

Tweets Search sample app with Compose ● Initial text ● Loading text ● Empty results text ● List of Results ● Error text @raulhernandezl

Slide 156

Slide 156 text

Lessons learned @raulhernandezl

Slide 157

Slide 157 text

Interoperability @raulhernandezl

Slide 158

Slide 158 text

Recomposition runs more times than one @raulhernandezl

Slide 159

Slide 159 text

remember would save a previous state in the View @raulhernandezl

Slide 160

Slide 160 text

Composition Local is the “simplest” way to pass Data Flow to the tree and its children @raulhernandezl

Slide 161

Slide 161 text

Composables should be Side Effects free @raulhernandezl

Slide 162

Slide 162 text

UDF likes states @raulhernandezl

Slide 163

Slide 163 text

Imperative vs Declarative programming @raulhernandezl

Slide 164

Slide 164 text

Why Compose? @raulhernandezl

Slide 165

Slide 165 text

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

Slide 166

Slide 166 text

Performance improvements @raulhernandezl

Slide 167

Slide 167 text

Testability @raulhernandezl

Slide 168

Slide 168 text

Reusability of UI components @raulhernandezl

Slide 169

Slide 169 text

Experimentation of new ways of working @raulhernandezl

Slide 170

Slide 170 text

Next Steps @raulhernandezl

Slide 171

Slide 171 text

Start adopting explicit state declarations TODAY @raulhernandezl

Slide 172

Slide 172 text

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

Slide 173

Slide 173 text

Special thanks @raulhernandezl Nick Butcher

Slide 174

Slide 174 text

Thank you. @raulhernandezl

Slide 175

Slide 175 text

Questions? @raulhernandezl

Slide 176

Slide 176 text

(Used) Dependencies note: this may change really quickly, read official sources to verify @raulhernandezl

Slide 177

Slide 177 text

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'

Slide 178

Slide 178 text

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'

Slide 179

Slide 179 text

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' }

Slide 180

Slide 180 text

Gradle’s wrapper dependencies for Jetpack Compose @raulhernandezl gradle/wrapper/gradle-wrapper.properties ... distributionUrl=https://services.gradle.org/distributions/gradle-7.0-bin .zip

Slide 181

Slide 181 text

Android Studio versioning @raulhernandezl Android Studio Bumblebee | 2021.1.1 Canary 3