A Brief Analysis of Declarative UI — Android — Bloom & Wild
In a lot of programming frameworks, when it comes to UI, we use an imperative paradigm by using specific statements to get references to the UI and change its state.
Switching between Imperative and Declarative paradigms can get complicated. Both Programming Paradigms end up doing the same, but in different ways.
Getting Started
In this article, I’ll share a brief analysis of both paradigms and show you a growing solution we’re using at Bloom & Wild: Google’s Modern Toolkit, Compose.
The programming language used in the paradigm examples is Kotlin.
Programming Paradigms
Programming paradigms are a way to classify programming languages based on their features.
Let’s have a look at these definitions:
Imperative Programming: uses statements that change a program’s state and focuses on describing how a program operates
Declarative Programming: expresses the logic of a computation without describing its control flow, focusing on the what and not on the how
Imagine the following scenario:
You have a list of numbers from 1 to 10. You want to go through the list, and get a new list from this one, with only even numbers.
Now... How would we do this, with each one of the paradigms?
Imperative Programming
Imperative Programming uses statements to change a program’s state and focuses on describing how a program operates, step by step.
With an initial list from 1 to 10, we go through all numbers in the list and each even number gets added to the listOfEvenNumbers.
Declarative Programming
Imperative Programming may be easier to reason for beginners. However, Declarative Programming allows us to write more readable code that reflects exactly what we want to see, becoming more concise.
With this paradigm, we focus on the desired result and not on how to compute it.
What does this have to do with Declarative UI?
Declarative UI
Compose
Compose is a modern toolkit, originally built having the Android Framework in mind.
I say originally built because, at the moment, it is possible not only to use Compose in Android but also on Desktop and Web. It is becoming more versatile.
Compose simplifies and accelerates UI development.
As Leland Richardson said in his article:
The expectations around UI development have grown. Today, we can’t build an app and meet the user’s needs without having a polished user interface including animation and motion. These requirements are things that didn’t exist when the current UI Toolkit was created.
Let’s have a look at some simple examples, using Android XML as a term of comparison and focusing only on their readability.
Imagine that you want to place two labels in the middle of the screen, one after another like a column:
With XML, we could do something like this:
Let’s be honest, it is hard to read and understand at first sight.
This is how you can do it with Compose 😍
If you would like to learn more about it, there are some materials you can check at the end of the article.
Mindset shift
The biggest mindset shift when moving from XML-based UI design to Jetpack Compose is thinking in a declarative, rather than imperative, way.
As discussed previously, with XML-based UI design, you typically specify how the UI should be laid out and what should happen when certain events occur (such as a button being clicked) by writing imperative code that explicitly defines these things.
With Jetpack Compose, on the other hand, you define the UI by declaring what it should look like and how it should behave, and the framework takes care of the rest.
Exposing States
Another important mindset shift when moving to Jetpack Compose is thinking in terms of states and exposing these states to our Composables.
In Jetpack Compose, the UI is built from small, reusable components called Composables. These Composables are functions that take in input values and return a UI element, and they can be composed together to build larger and more complex UIs.
To update the UI in response to changes in the underlying data, you need to expose the relevant states to your Composables, so that they can react to these changes and update the UI accordingly.
Recomposition
Recomposition refers to the process of recomputing and updating the UI in response to changes in the app’s state or user interactions.
When the state of the app changes, Jetpack Compose automatically recomposes the UI by calling the composable functions that depend on the changed state.
During recomposition, Jetpack Compose compares the current state of the app with the previous state and calculates the differences. It then applies only the necessary changes to update the UI efficiently.
The recomposition process in Jetpack Compose is fast and efficient because it avoids unnecessary redraws and updates.
At Bloom & Wild 🌸
Initially, at Bloom & Wild, most of our layouts were written in XML.
Now, we have a big part of them written with Jetpack Compose!
However, this brought some challenges…
Interoperability Challenges
Theming: Since Jetpack Compose uses a new theming system that is not compatible with the traditional Android XML theming system, this can make it difficult to ensure consistency across your app’s UI when mixing Compose and XML views.
State management: With Jetpack Compose, we expose the relevant states to our Composables. With XML layouts rely on callbacks and listeners to manage state. This can make it challenging to keep the state of Compose and XML views in sync.
However, we believe that the advantages overcome these challenges.
Advantages
Increased productivity: The declarative approach to UI development allows us to have a faster iteration and more efficient development.
Improved performance: Designed to be more performant than traditional Android UI development approaches. This results in better overall app performance, even when mixing Compose and XML views.
Easier maintenance: It's easier to maintain complex UIs over time. Simplifies the app’s architecture, making it easier to debug and maintain.
Seamless migration: The ability to mix it with XML views makes it easier to gradually migrate the app’s UI to Jetpack Compose, without requiring a complete rewrite of the existing XML layouts.
One of our use cases
One of the most considerable sections we have refactored to Jetpack Compose was the page where users land when opening the app, the Products List.
Separating the components
If we separate this screen into small parts, we got something like the following:
Filters — We opted not to change these to Jetpack Compose for now
List Header — Sort functionality, items count, and list view type switch
@Composable
fun ListHeader(...) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
SortButton(
onSortClicked = { ... },
)
ItemCount(count = ...)
SwitchListView(
listViewType = ...,
onSingleViewClicked = ...,
onGridViewClicked = ...
)
}
}
By using the Row composable, we managed to align everything that’s inside in a horizontal container.
List
@Composable
fun List(...) {
when (listViewType) {
ListViewType.SINGLE -> {
SingleColumnView(...)
}
ListViewType.GRID -> {
GridColumnView(...)
}
}
}
@Composable
fun SingleColumnView(...) {
LazyColumn(...) {
items(
count = items.size,
key = { index -> items[index].id },
itemContent = { index ->
ListItem(...)
},
)
}
}
@Composable
fun GridColumnView(...) {
LazyVerticalGrid() {
items(
count = items.size,
key = { index -> items[index].id },
itemContent = { index ->
ListItem(...)
},
)
}
}
Jetpack Compose provides an out-of-the-box way to display items in a single-column list or in a grid column. This came up in handy since we use a multiple-view display on our list.
Conclusion
In conclusion, Jetpack Compose has greatly improved the readability of our codebase and has streamlined the development process for our Android app.
With its declarative UI approach and Kotlin-based syntax, we were able to more easily understand and modify the structure and layout of our app’s user interface.
The use of Jetpack Compose has also allowed us to more easily write and maintain responsive and dynamic UIs, as we can define UI behaviors and interactions directly within our composable functions.