Getting ready for Declarative UIs — Part 2 — Implementing Unidirectional Data Flow

Raul Hernandez Lopez
ProAndroidDev
Published in
10 min readMar 2, 2021

--

Photo by Derek Oyen on Unsplash

(This article was featured at Compose #7 Digest, Android #456 & Kotlin #240 Weekly & jetc.dev #55)

The most important part of an iceberg is not what you can observe from the surface. What really matters is what is under it. That is what will make it float above the surface.

Comparing an iceberg with Declarative UIs, we can tell that essentially our iceberg would be powered by a Reactive approach to producing successful updates. Updates that came from a change, preferably executing in a single direction. These are indeed the foundations of Unidirectional Data Flow (UDF).

Throughout this article, I will elaborate on the Implementation details from the Business layer to the View layer. By exploring how we could implement it in the simplest terms as well as avoiding change our existing design and architecture.

I assume you resumed this series from:

UI performs an Action

Note that I won’t dig much into the initial data flow implementation: the one starting from the View layer to the Business layer. However, for that particular one, I’d encourage you to follow some steps, defined in a previous series:

  • 1) Define your Action. Meaning that the first direction data flow is already in place. Our Imperative UI produces the intended action from the SearchView. Then the ViewDelegate triggers a query of interest (for more details about Action Data Flow read this article).
  • 2) The UseCase (more details about what a UseCase means in this article) collects values that come from the Data layer. Concretely, a Kotlin Flow or open stream.
Action Unidirectional Data Flow

Now let’s define a State from the UseCase.

Action is transformed into a unique State

The UseCase continues our UDF implementation. Driving in one direction and its Imperative UI (no worries, this is temporarily Imperative, we are aiming to change this step by step).

State Unidirectional Data Flow

Note that I won’t explain in detail all the awesome features of StateFlow, however, I will focus on explaining how to be ready for Declarative UIs. For more details about StateFlow I’d encourage you to read this article called Synchronous Communication with the UI using StateFlow.

Let’s refresh the important points from the previous introductory article.

We defined a well set of UI states.

Modelling UI states

  • A Loading spinner would become a Loading UI state.
  • Empty results text would become an Empty UI state.
  • The list of results would become a List UI state.
  • Error message text would become an Error UI state.

We have a set of read-only values in TweetsUIState, immutable by default since all use val attributes.

TweetsUIState sealed class for UI states

Propagation from the UseCase to the View

We can follow up those modelled UI states upstream:

  • From the UseCase (Kotlin) up to the Presenter (Java)
  • From that Presenter to the ViewDelegate (Kotlin)
  • From the ViewDelegate to the View, actually to the StateFlowHandler (Kotlin) — remember we need an associated helper to the View precisely because that was in Java and Coroutines need a Kotlin class for them.
Upstream sequence of propagations of the `StateFlow` values

UseCase propagation of StateFlow

Getting hands-on with the implementation. Normally my (opinionated) preference is to create as small as possible interfaces, following the Interface Segregation principle from the SOLID principles by creating a minimal UseCaseStateFlow interface. Which has 3 functions:

  • execute() with a query as an input parameter.
  • cancel() to stop ongoing coroutines.
  • getStateFlow() to expose the StateFlow with a generic parameter to encapsulate that of that StateFlow value. Meaning that StateFlow is an interface and provides a read-only value.
UseCaseStateFlow<T> interface segregation
  • We just need to initialise the MutableStateFlow with an initial InitialIdleState (since it always emits the latest value).
  • Then we set the state with its property method or setter using tweetsStateFlow.value.
  • The first representative state LoadingUIState is launched with onStart{}.
  • Then for each emission of the stream, we use onEach{}.
  • When results are empty, the state flow is associated with EmptyUIState.
  • When there are values, the state flow is associated with ListResultsUIState.
  • Finally, whether there are any errors, we use the catch{}, in order to dispatch an ErrorUIState.

We expose the immutable StateFlow with that return type for getStateFlow().

SearchTweetUsecase modelling states

From this point, we communicate straight upstream to the presentation layers.

Presentation propagation of StateFlow

SearchTweetPresenter ’s StateFlow propagation is as easy as exposing the public method for any consumers to use.

SearchTweetPresenter’s StateFlow propagation by its public method

Likewise for the SearchViewDelegate ‘s propagation.

SearchViewDelegate’s StateFlow propagation by its public method

After all possible connections are exposed, it becomes clearer how that gets connected in combination with the view. The SearchViewDelegate includes a reference to that view, which would be an extension function for it.

SearchViewDelegate contains a Presenter injected and initialises the flows and views

The following view initialises the SearchStateHandler with a reference to the StateFlow coming from the SearchViewDelegate and a reference to TweetsListUIFragment.

TweetsListUIFragment initialises StateFlow and Views associated with it

Now it’s time for StateFlow‘s collection, which of course, requires a Kotlin class. SearchStateHandler is written in Kotlin, plus it helps to decouple our logic from the view.

The StateFlow‘s collection is straight forward. We just need to use onEach to get the uiState as well as launchIn with the preferred CoroutineScope of your choice.

SearchStateHandler StateFlow collection

Let’s check now what’s behind handleStates extension function.

Imperative (Stateful) UI Rendering

In my opinion, the first step when we just have Imperative UIs over the place is to keep the intended states working seamlessly with them too. I will explain in detail the ListResultsUIState case in a moment. For this state, we need to show results. But what does imply except rendering a list of tweets?

TweetsListFragmentUI extension function with showResults method

Notice that for simplicity, I won’t extend on RecyclerView (RV) specific implementations.

First, we show results in an Imperative UI. The first step is to hide any other element which currently is visible. This happens for the first state, “Loading data”.

Loading message becomes invisible

Next, we also hide any error if it is also visible.

Error message becomes invisible

But we are not done just yet. We also need to make our RV visible. Typically a RV contains a LayoutManager and an ItemDecorator for its setup.

RecyclerView becomes visible and contains a LayoutManager & ItemDecorator in it

Of course, once the state was delivered from the chain which we defined previously what we will also need to complete is the RV setup. This needs any items provided by the list of tweets. The entity in charge to fill those elements is called Adapter. For each item, we have a ViewHolder that defines each item’s visual components too.

SearchStateHandler delivers the ListUIState from StateFlow to Adapter, containing the tweets.

Very briefly, it would look like the next code snippet. There we hide the loader, the error, show the list and update the list with the list of items (tweets).

Now you can probably foresee why we call Imperative UIs “Stateful”. Because each view needs to handle its own state, either visible or invisible at any time.

Good news, now it’s time for us to modify the existing Imperative UI.

Declarative (Stateless) UI migration

Because we are emitting states that are one single screen related and match those ones, that could be applicable to Jetpack Compose too.

State UDF: The Imperative UI can be replaced with a Declarative UI

I would suggest starting small and dig into the XML for a bit. Then removing anything eligible to be replaced by those states and combine as a whole composable when possible.

For the next scenario, I prefer to keep a ConstraintLayout, in addition to the top existing AppBarLayout with its Toolbar. Then replacing anything else like TextView or RecyclerView with ComposeView instead. Thus, all result output states will be displayed in a ComposeView.

interoperability in the XML with ComposeView

After this first step, we can that ComposeView with findViewById(). After that, we can use it in other decoupled entities like SearchComposablesUI.

TweetsListUIFragment in Java

Previously a fragment (aka: this) reference was an input parameter for initStateFlowAndViews. Now it is replaced with a reference to SearchComposablesUI.

SearchStateHandler uses searchComposablesUI instead of the reference to the fragment

That means that SearchStateHandler is able to help SearchComposablesUI to start composing UIs.

SearchComposablesUI starts composing views as the UI state comes

The next element from this chain is SearchComposablesUI.

It contains a reference to the ComposeView and the SearchTweetActivity (for interoperability).

In Jetpack Compose we need to start from setContent{}. Where a theme is defined, otherwise we must use a MaterialTheme.

Within the theme, we can add our first @Composable UI. For instance, I defined StatesUI as the main composable. A composable can only be in another composable or a MaterialTheme. Likewise, Kotlin Coroutines’s suspend functions only can be allocated in another suspend or a CoroutineScope.

SearchComposablesUI start composing views

In a nutshell, this composable will handle states to a more specialised composable for each UI state.

Do not worry, we’ll dig into more details about how to use an inner composable in the next step.

Declarative (Stateless) UI handling alternatives

Another optimisation, in my opinion, is removing this StateFlowHandler on behalf of a UIStateHandler which can do the same task but directly.

Meaning we can also replace that handler connected to the view and using fewer entities.

UIStateHandler handles UI State to Declarative UI, StateFlowHandler can be removed

This translated to the UDF diagram looks like this:

UIStateHandler replaces the SearchStateFlowHandler

That means that we can replace the previous SearchComposablesUI to become a SearchUIStateHandler. This would simplify the number of dependencies we need to inject. As an added value, it will just need a StateFlow.

TweetsListFragmentUI with a StateFlow for initialisation

Now StatesUI won’t need to pass a Stateflow parameter because it’s contained into the SearchUIStateHandler.

When StatesUI receives a new state a recomposition happens. This is due to the fact that stateFlow has distinguished a different state than the previous one. Then it will directly use the collectAsState() (now stable from Compose beta-01) to collect a ready to use State.

StatesUI passes this State value to a StateUIValue composable. Then we can start playing with each individual state.

SearchUIStateHandler collects state into StateUI and drives it to StateUIValue composable

Now let’s quickly look at how to render those Declarative UIs with Jetpack Compose.

Declarative (Stateless) UI Rendering

The first thing we need to think is that our ListResultsUIState needs to render a TweetsList composable with a list of tweets.

SearchUIStateHandler uses StateUIValue to render specific UI states

The TweetsList composable is defined with a LazyVerticalGrid (@ExperimentalFoundationApi in the latest Compose beta-01, this may change soon) which embeds into its LazyGridScope interface a function to add the items. Among its trailing available properties:

  • A count of elements defined with an Int value.
  • itemContent, where we define the content for the TweetBox.
LazyVerticalGrid and what is composed by.

To create a LazyVerticalGrid, we need to adjust its properties by adding a modifier with our preferred configuration. For adjusting how many cells we want to allocate per row, we can use GridCells.Fixed. However, there is an option to make it adaptive to a concrete width measured in dp by means of GridCells.Adaptive.

Using count, we assign a total number of tweets. As well as itemContent uses the index associated with the current item. Whose value will be assigned to tweet by means of tweets[index].

TweetsList is defined as a LazyVerticalGrid

TweetBox is composed of a Box which has a .clickable() modifier (to navigate to the tweet detail). Furthermore, it contains a certain border() with attributes such as shape, width or color among others.

Within its elements, there is a Column type. That Column depicts a space between (SpaceBetween) elements and will centre them horizontally (CenterHorizontally).

Talking about the actual Column‘s content. I’m using a CoilImage at the top, a Spacer right in the middle and a Text at the bottom.

Box contains a Colum, Column contains CoilImage, Spacer & Text

The resulting @Composable for TweetBox will look like follows:

TweetBox with a Colum composed by CoilImage, Spacer and Text, all separated with space in between as well as the box is clickable

This will render a grid-like screen that contains two separate columns per row with a border surrounded by a shape button-like. Each item at the same time contains an image at the top, the space in between and the complete name of a Twitter user.

Grid view for the hashtag #TheAndroidShow

Are we done with this?

We still need to complete the remaining states, but no worries, this is going to be the last thing that we need.

Likewise ListResultsUIState, we need to cover the remaining scenarios for LoadingUIState, EmptyUIState, ErrorUIState or IdleState. For all those cases, I made it simple enough and used a generic CenteredText for most of them, which is essentially a centered Text. TopText is another text-like variant, which is aligned to the top and used for the initial idle state.

Furthermore, it’s really awesome that we can use our existing string values from strings.xml with stringResource(R.string.xxxx).

StateUIValue handles different Composables depending on State

Those well-defined UI states render each different screen or composable. We say that composable is stateless because it needs to know nothing about visibilities or properties like that. It renders as the UI state tells and don’t care again about their state unless they need to render again due to Recomposition.

Finally, it will look like this when searching for “#TheAndroidShow”:

A user’s search intention of #TheAndroidShow and clicking one of the tweets to see its detail

With much less code than using (classic) Imperative Android Views, we have really powerful Declarative UIs with Jetpack Compose.

That’s a wrap. I hope you enjoyed this second article. The next one will be about the lessons learned at the end of exploring Jetpack Compose.

If you liked this article, clap and share it, please!

Cheers!

Raul Hernandez Lopez

GitHub | Twitter | Gists

I want to give a special thanks to Vova Buberenko & Joe Birch for the good review of this article and to make it more readable to anyone!

The last article of this series about the “Why” was:

For more practical resources related to this topic, this is the Fosdem Conference’s 2021 talk I gave back in February:

--

--

Senior Staff Software Engineer. Continuous learner, sometimes runner, some time speaker & open minded. Opinions my own.