Blog Infos
Author
Published
Topics
,
Published
Our UI layer should be dumb with minimal code and minimal logic that’s what everybody says but still, how can we achieve that?
Passive View

Martin Fowler defines a Passive View as “A screen and components with all application specific behavior extracted into a controller so that the widgets have their state controlled entirely by controller.”

 

In other terms, we are encouraged to make our UI code as dumb as possible with very little logic and complexity. This way we make sure the UI code is easily testable, maintainable, and scalable. And we also make sure concerns are separated (no business logic in the UI code) and the UI sticks to the responsibility of displaying things and handling user interactions.
This idea has always been pushed and supported among Android developers but still, we usually keep seeing UI code with some level of complexity. Be it with Compose or with the View System, it is still easy to end up with quite a complex UI code with a lot of logic that could live in a Controller.

As a rule of thumb, If you have a lot of if-else statements, loops/iterations, and some amount of logic that is not specifically UI related, then your view is probably doing too much, it’s probably not Passive.

This article is not necessarily about the details of the Passive View pattern but mostly about the idea of putting together efforts to make a View passive with very less logic or complexity.

In this article, I will discuss my own vision on things I think are important to keep in mind when writing UI code in order to avoid complexity or at least keep things simple and stupid there.

UI State representation

This topic is not new anymore. I think we don’t need to explain to Android developers why and how they should represent their UI state anymore. Even the official Android documentation was updated to now show how to properly represent the UI state.

https://developer.android.com/jetpack/guide/ui-layer

TLDR; Basically, do not have multiple objects in your ViewModel to represent the UI state, but use a single one that is mutable, mutate its value in the ViewModel and observe the changes from the View side. Also, as much as possible, use StateFlow instead of LiveData.

So do not do this:

class MyBadViewModel: ViewModel() {
    val isLoading: MutableLiveData<Boolean> = MutableLiveData(true)
    val isPremium: MutableLiveData<Boolean> = MutableLiveData(false)
    val isError: MutableLiveData<Boolean> = MutableLiveData(false)
    val userMessages: MutableLiveData<List<Message>> =   MutableLiveData(listOf())
...
}

But do this instead (check the Android official doc for better examples):

data class NewsUiState(
    val isLoading: Boolean = true,
    val isPremium: Boolean = false, 
    val isError: Boolean = false,    
    val userMessages: List<Message> = listOf()
)
class MyGoodViewModel: ViewModel() {
    private val _state: MutableStateFlow<NewsUiState> = MutableStateFlow(NewsUiState())
    val state: StateFlow<QuestionsUIState> = _state.asStateFlow() 
...
}

With this, observing the UI state on the View side will look much cleaner because the view will have only one object to observe instead of four. This also helps to enforce a proper Unidirectional Data Flow.

UI models: avoid “Backend driven development”

If you look closely at the piece of code above, you will see there is a Message class which is the type of the list of userMessages. In a typical app, a Message will certainly be a domain model being returned by the backend representing a user message. Now, mobile engineers should be very careful with this kind of information because they can easily mess up your UI code. Let me explain.

To properly explain this, I will take an example of an imaginary banking app whose backend returns a list of transactions (or replace it with Message if you want) and the Mobile app must display it as per this design:

Now, this UI only shows the list of transactions but the feature request is that when the user clicks on a transaction, they are taken to the transaction details page with much more information like the fees, the conversion rate, the transaction number, etc. So backend will return the full Transaction object and mobile will display only the info required on the list and display all of them on the details page by sending the Transaction object through the Intent, easy peasy.

Let’s say this is the object the backend returns

data class Transaction(
val id: Long?,
val amount: Double?,
val currency: Currency?,
val reference: String?,
val transactiontype: String?,
val direction: String?,
val correspondent: TransactionCorrespondent?,
val beginDate: Long? = 0,
val endDate: Long? = 0,
val fees: Double? = 0.0,
val taxes: Double? = 0.0,
val commission: Double? = 0.0,
val commissionTaxes: Double? = 0.0,
val status: String?,
val sequence: String?,
val itemType: String?,
val dateDirection: String?,
val rate: Double,
val total: Double?,
val totalFees: Double?
)
view raw Transaction.kt hosted with ❤ by GitHub

This might not be very representative of the reality

 

As you can see, this is a very big object with a lot of information and not all of them are displayed on the transaction list.

My opinion, and maybe you will share the same, is that mobile apps should have their own models that fit better with their architecture and UI design and not blindly reusing on all levels what the backend is returning them. Otherwise, you may end up with what I call “backend driven development”. The Transaction object returned by the backend is what you can call a Remote model and it should be mapped into a domain model (if necessary) and then later must be mapped into a UI model that will be much easy to use on the UI layer. Domain modeling can be a topic for another article but here, the UI model is the one that is in our interest.

If we look at the UI design above again, we can see that visually, a Transaction item has only 6 UI elements on the screen:

  1. The transaction title (that could be a Bank or recipient User)
  2. An image/icon (logo of the bank or recipient photo)
  3. The transaction type (bank transfer or merchant payment)
  4. The amount of the transaction
  5. The date of the transaction
  6. The color of the amount (indicating CREDIT or DEBIT to/from the account)

So we can stupidly create a UI model that will look like this:

data class TransactionUI(
val title: String = "",
val type: String = "",
val image: String = "",
val amount: String = "",
val date: String = "",
@ColorRes val color: Int = R.color.yellow
)

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Managing a state might be a challenge. Managing the state with hundreds of updates and constant recomposition of floating emojis is a challenge indeed.
Watch Video

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Piotr Prus
Android developer

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Piotr Prus
Android developer

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Piotr Prus
Android developer

Jobs

As you can see this model is much more simple and will lead to a simpler and cleaner code on the View side. Why? Because you can simply and directly assign the values to the UI elements without any transformation or logic.

binding.title = transaction.title
binding.type = transaction.type
binding.image.load(transaction.image)
binding.amount = transaction.amount
binding.date = transaction.date

 

To be able to achieve this, you must build the transaction UI object by doing the necessary transformation beforehand. That means that you must have formated the date, which was originally a timestamp Long, into the right format and made it a string like this: 01/02/2003. Or transformed the amount which was represented by a Double and a Currency object plus the direction information which will help to display – or + in front of the amount, all of that transformed into a simple string like this: +320.00 CVE, etc…

These transformations produce what we can call the “ready to display” format.
And notice that we mostly use primitive types in the UI model.
First, we use strings and that’s because everything that is displayed on the screen is usually text, and what’s the right data format for text? You got it.
We also use integers for representing colors, string resources, Drawables, and other Android resources. We would add the appropriate annotations like @ColorRes to make sure it is safe. And then, we can use it like this:

binding.amount.setTextColor(state.transaction.color)

You can also use Android framework types for resources like Color which you can use in Compose:

data class TransactionUI(
val title: String = "",
val type: String = "",
val image: String = "",
val amount: String = "",
val date: String = "",
val color: Color = Color.Yellow
)

And use it like this:

Text(text= state.transaction.title, color = state.transaction.color)

You can also (or should) choose to make the values nullable and provide null as the default value. This is up to you or will depend on the situation.

Without the simple UI model, you’d have to send the complex Remote model to your activity, fragment, or adapter and you’d have to do all the formatting and transformation logic there which will certainly increase the complexity.

Make it Parcelable if needed

Because the UI model is so simple and is based on primitive types only, you can then easily make it Parcelable if you have to pass it around your activities or fragments.

Use UI mappers to handle the transformations

Now you may ask where do we do these transformations? And some of you will certainly say ViewModel. Yes, you can do that but that’s not my favorite place for it. One technique that I favor is to use a UI mapper class that is responsible for converting the Transaction Remote model into the Transaction UI model. The mapper would have all the transformation and formatting logic to make this possible.

UI Mappers: how to build them?

A UI mapper is a component that takes a domain or data model object and creates a UI model object out of it. The UI model object produced should be “ready to display” as we mentioned previously.
We can isolate the transformation and formatting logic into classes like AmountFormatter, DateFormatter, etc, and inject those formatters into the mapper.
Also, the mapper may need the context to be able to work with android resources for example. In this case, we can inject the Context or my favorite approach is to build a Resource provider class that has Context injected and is responsible for providing Android resources like string, colors, dimensions, etc…

Back to the UI state

So if you ask how does this fit into the UI state representation? Well, it’s quite simple:

data class TransactionsUiState(
    val isLoading: Boolean = true,
    val isError: Boolean = false, 
    val transactions: List<TransactionUI>? = null
)
Conclusion

In this article, we have discussed in detail some of my ideas on how to slightly make your UI code less complex. Of course, there are so many other things you can do to keep your UI code clean and free of logic.
Remember, as a rule of thumb, If you have a lot of if-else statements, loops/iterations, and some amount of logic that is not specifically UI related, then your view is probably doing too much, it’s probably not Passive.

I wrote another article about improving your UI code when Implementing UDF and how to handle user actions differently.
You can check it out here.

 

This article was originally published on proandroiddev.com on April 29, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
In this part of the series, we will plan our first screen in Jetpack…
READ MORE
blog
We’ll be selecting a time whenever a user presses a number key. Following points…
READ MORE
blog
Ask yourself a fairly standard question for any interview to fill an Android position:…
READ MORE
blog
This is part of a multi-part series about learning to use Jetpack Compose through…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu