Getting ready for Declarative UIs — Part 3 — Why Declarative UIs on Android?

Raul Hernandez Lopez
ProAndroidDev
Published in
8 min readApr 21, 2021

--

Photo by Johny vino on Unsplash

During this article, we’ll discuss reasons why Declarative UIs could be a choice compared with Imperative UIs and really “Why” it could be a choice that matters on Android.

(This article was featured at Compose #14 Digest, Android #463, Kotlin #247 Weekly, jetc.dev #62 & Karumi April’21)

A State Machine

A state machine or finite-state machine, understanding the term “finite-state” like a limited number of states. By definition, it is a mathematical model of computation. That actually is a simplification of the very famous Turing Machine, basically, a state machine would be a subset of it. When there is a finite set of states and we choose a deterministic finite-state machine, then it will give us the opportunity to predict an output from a concrete input.

An ATM would be with no doubt an example of a state machine, where we can start a transaction with our bank card and there is a limited number of actions/transactions that we can do after the PIN was entered as well as validated. Otherwise, the PIN validation reached a maximum number of attempts and goes to the final state, that last state happens when the bank card is ejected.

Simplified Transaction and PIN validation finite-state machine diagram

In the diagram above, we can notice a combination of states, corresponding to the workflow on each arrow. When each “action” is transformed into a “state” such as “validated”, “rejected” (with a number of attempts), “rejected” (with the maximum number of attempts), “verified” or “finished”.

A state machine also engages really well with a really important term in functional programming, do you remember “referential transparency”?

the ability to make larger functions out of smaller ones through composition and produce always the same output for a given input

You may wonder, why is all of this important for Declarative UIs?
Thinking in Declarative UI’s nature, they benefit from those concepts.

If you recall the first article of this series mentioned Unidirectional Data Flow principles. Once modeled a series of UI states depending on the needs of the current action. Those actions were transformed into states that are rendered in the UI.

UDF triangle = UI -> Action -> State -> UI

If we think more about it, we basically created a state machine, which gives a finite number of states, therefore a finite-state machine.

Why aren’t Imperative UIs good enough if we worked with them for years and years in Android applications?

For that, we necessarily need to first compare both Declarative and Imperative paradigms.

Imperative vs Declarative paradigm

By definition, in Imperative programming, we use statements to change a program’s state. Meaning that the Imperative paradigm focuses on “how” we achieve it to get to a particular state.

This is not really a bad thing, is it?

Thinking about Imperative UIs more specifically, their complexity increases more when each of them manages its own state as well as a need to orchestrate them. A View typically associates one or more variables that define its state. For instance when switching a button from off to on and vice-versa. Now, imagine it having a really complex UI with several nested views on multiple XML and custom views. Then after that think about it handling all states separately and saving some intermediate state for each of them that none of the parents knows about. Precisely at that moment in time is when we probably regret it and when the real issue comes from. We don’t have a deterministic approach, either a finite set of states by then.

Now, thinking on Declarative UIs, the important concept is the “what”, not the “how”, there is no real reason to need to describe “how” we want it on the Declarative paradigm. We’ll look at “Why” in a bit!

To recall some important points made in Implementing Declarative UIs. We used to tell whether a view is hidden or shown. Thus, the view saved that information and changed its own state.

Differently, on Declarative UIs that is not needed. A screen renders as the state clearly mandates. This means that just by introducing a set of rules which are understood by our UI logic and state machine, we will create on-demand UI views.

Now, narrowing down all those concepts to Android specifically. We must consider if we have all we need to provide a more modern UI or not.

The first thing that we may want to check is Interoperability.

Interoperability

Thinking on Android, really Jetpack Compose Interoperability is great.

For instance, when we have a UI defining a screen and we want to move bits and pieces of that screen to Compose, we might have it all into some embedded XML files. Luckily that is compatible and could be easily integrated on a classic XML layout by using ComposeView. This use case is needed when we want to integrate it into an existing layout. For an extended example read more here.

However, in the opposite direction, whether we created a shiny new Compose screen and we need to reuse a real big custom view that was created in the old years, do not worry, we can also include it with AndroidView.

SimpleTweetBoxWithCompatibleAndroidView has a classic TextView

For more content about Interoperability, I recommend this article by Adam Bennett.

Compose vs Android Classic View system

Unbundled and part of the OS

An important difference between Compose and the Classic View system is that Compose is unbundled. Meaning that it is not part of the Android OS, that’s because it’s a library. This also means that probably we’ll need to live with both kinds of paradigms and views anyways.

Parallel or sequential

  • Compose is not meant to be executed sequentially as per what Classic Views do.
  • What it’s called Recomposition runs frequently as well as can be run in parallel and happen anytime.

If our existing UI depends on a certain sequential order to render UI elements, that wouldn’t work very well with Compose.

We just talked about recomposition, but what if we need to save states across different recompositions?

For that purpose, we have remember , which would save a local state, that would be used after recomposition and will be kept for us.
Another interesting concept is called Composition Local (CL), it is considered the simplest way to pass a data flow through the tree and its children without having to explicitly add a parameter to all composable functions (I won’t extend on this concept since it could perfectly be a full blog post, perhaps if you are familiar with Dependency Injection that would look like a familiar thing to you).

Side Effects

What if we want to add a side effect (like a database operation or network request) on one of our composable functions? we are covered with SideEffect and other Effect Handlers. Those run after every recomposition. For further reading, I recommend this article by Jorge Castillo.

Lines of code

On classic views, we have to extend let’s say the massive file called View. Typically we need to start adding more and more layers to the view in order to achieve a very minimal change. Which it’s not ideal.

That and switching from XML files to Java/Kotlin code all the time. Which determines a barrier between both, a barrier that vanished with View-Binding anyways, however, it marked a clear separation between them.

On the one hand, the hidden cost of using code for both Compose (instead of XML) and anything else is that now we need to be aware of when/how to create proper separation of concerns. Thanks, Unidirectional Data Flow.

On the other hand, Compose definitely wins on the number of lines of code needed for a simple UI.

Stateless and Stateful

Compose aims to be stateless. Meaning that there is no state associated with the view, opposite to the classic view as we mentioned earlier. So far, we learned about sending immutable data structures in order to activate a recomposition of the UI as well as take advantage of Unidirectional Data Flow. In Compose we don’t set UI elements visibility, the view when is composed either is or is not in the UI tree or slot table.
Classic views are stateful. Usually, a view has a state associated to it, for instance, its visibility. It’s prone to have mutable states internally that are changed depending on a certain change.

Kotlin and Java Interop

  • Compose only works in Kotlin.
  • Classic views can run in both Kotlin and Java codebases.

Why Declarative UIs on Android?

Performance improvements

Compose aims to run really fast because it can run every recomposition of the tree, in parallel. However, this doesn’t guarantee an order on those recompositions.

Despite there is space for improvement in performance just yet. The Compose team is focusing on performance improvements currently.

Code Testability

It should be easier to achieve since Compose is Declarative. If we pass only immutable data structures by default, we can test different components in isolation. Let’s assume that all deeper layers were already tested. In that case, we can directly perform UI tests, either screenshot tests with Karumi’s library called Shot or Espresso-like UI tests.

Reusability

Reusability of UI components, it’s fairly easy to create generic components that can be reused and more importantly, extended.

Building a new component takes much fewer lines of code than with classic Android views. See the example of a RecyclerView on Android from Implementing UDF at Part 2.

Experimentation

Declarative UIs allow easier experimentation and with this, new ways of working. For instance, Compose enables Server Driven UIs (thanks for the good article Joe!) without an endless number of classes.

Conclusions

I’d encourage you to start adopting explicit state declarations TODAY. It certainly enables both Declarative and Imperative UIs, help our systems to be more deterministic (predict an output from a certain input) or finite-state machine-ish and therefore, more predictable and error-proof.

Declarative UIs are definitely the future. Indeed they have been a fact for the Web with React JS, on multi-platform with Flutter and for iOS with SwiftUI. Without forgetting about Kotlin Multiplatform and Compose Desktop.

Despite on Android, we are going to need OS views for sure to live with for a while, I can see a bright future when we will use Compose more and more.

That’s a wrap. I hope you enjoyed this last article of the series as much as I did writing it.

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 Adam Bennett & Jossi Wolf (follow them) for the good review of this article and to make it more readable to anyone!

Previous articles of this series were:

--

--

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