How to maximize Android’s UI reusability — 5 common mistakes
During the last few months, I had the opportunity to revisit some of our existing UI at Groupon. As part of this process, we started by identifying what is good in our UI and what are our most common mistakes, with the hope of taking Groupon's UI to the next level.
What makes UI good? Good is relative but it was easy to agree on three main principles that can be applied to any software:
- Readable. Good UI components are easy to read. No need to visit Android Studio’s design tab, the xml is self-explanatory.
- Testable. Every UI component can be easily tested without depending or external components.
- Reusable. UI components can be reused and extended without changing their original behaviors.
In this article, we will go through some common mistakes that affect our UI’s reusability and see how reusable UIs can make our code more readable and testable.
1. Business model’s in custom views
Sometimes, in order to save time and avoid duplicated code, we decide to reuse in our views, the same models that we use in our network and database models. Let’s take an example:
userApiClient to pull our user’s information from backend:
On success, we receive the model:
And we pass that same model to our custom view
UserView, that will show the user’s name and address. This may look like an easy win but it’s not. It goes against our reusability principle and can cause serious problems in the long term. Let’s see why.
Why is this bad?
- The most important reason why we shouldn’t reuse our backend models is that we will not be able to reuse our view in a different part of the app. We are coupling our UI with an endpoint response and now we need that endpoint to render our UI.
- The backend model probably has more information than our view needs. Extra information means, extra complexity. The more information the model contains, the easier it is to use an incorrect field. In the example above, we could have used a simplified model:
- What happens if backend changes? We tend to assume that the backend services supporting our application will never change. This is definitely not true, our backend will change and we need to be ready for it. We shouldn’t be changing our UI just because something changed in the backend.
How to fix it?
Fixing this problem is easy but it requires some extra code. We will need to create a UI model and a method to transform the backend model into the UI model.
In our example, we will convert
UserUIModel and pass it as a parameter to
Our new implementation requires some extra plumbing and adds the complexity of a new transformation but this is a small price to pay to decouple our UI from our backend. Our new design prevents us from sending unnecessary data to our views and allows us to reuse our views wherever we need them.
2. Monolithic views
Have you ever opened a layout file and see a huge document containing all the activity’s UI? This is, unfortunately, a very common pattern in Android that causes us a lot of troubles.
Why is this bad?
It goes against our three principles:
- These files are really hard to read, too much information and too many layers of components living together.
- It’s not possible to test each part of the xml individually. We tend to only test our UI as part of integration tests executed in espresso. Integration tests are great but they mix UI and business logic, making hard to know what is the underlying cause of an error. By breaking our monolithic xmls into small custom views and removing all the business logic from our UI, we enable UI only tests. These tests can detect issues in our UI at a more finer granularity level than regular integration tests.
- We can’t reuse individual parts of our xml, this forces us to copy and paste those individual components in order to reuse them in a different screen. Over time, the duplicated components will start diverging, causing inconsistency in our app.
How to fix it?
Building all our UI logic in one xml file is the equivalent of building all our business logic in the activity class. We should break our UI in smaller pieces, in the same way, that we build DAOs, models and backend clients for our business logic.
Custom views and the <include> and <merge> tags are our best friends. We should always use them to break our UI at the beginning of any new feature. Waiting for a UI component to be reused in order to break it out of a monolithic xml can cause serious issues. At that time, our UI will be heavily coupled with our activity/fragment, making the refactor harder and endangering existing functionalities of our app.
Let’s see an example inspired by a real layout of an open source project. I have removed properties and added comments to make the layout easier to read:
Even after removing properties and adding comments, reading an xml like this is hard. There are several layers of nested layouts, making hard to understand where each component is placed. Without the comments, is hard to tell how the different tags are related and what they represent.
In the xml we can identify at least 6 well-defined UI components. Let’s see how this layout will look like if we create a custom view for these components:
By creating a custom view for each of our views we simplified our code, made our views reusable and prevented the side effects of future refactors. In this new implementation, the comments are no longer necessary because our code is self-descriptive.
Think how easy it will be to implement a new progress animation if all our activities use
ProgressOverlay. It will only require to change one class vs all our activities using the monolithic approach.
Groupon’s approach of favoring small UI components over monolithic xmls it is inspired by Brad Frost’s book Atomic Design. I recommend taking a look at this book, especially if you are passionate about UI.
3. Business logic in custom views
In the previous point, we talked about the benefits of using custom views, custom views are an amazing tool but if we don’t use them carefully they can be a double-edged sword. Coupling our business logic to a custom view it is surprisingly easy. Adding logs, AB tests, and decision-making logic may look a great way to compartmentalize our code but it is not.
Why is this bad?
- Logic in our views makes it harder for us to reuse them as the logic is coupling our view with our current use case. In order for our views to be fully reusable, they should remain simple and agnostic to the business logic. A well-implemented view should only receive a state and render it without taking any kind of decision.
- Adding business logic to our views makes it harder to test our views. Well-implemented views only depend on smaller UI components. They can be easily tested in isolation as part of an automation test. Good automation coverage, at the view level, will help us detect earlier undesired side effects of refactors and code changes in our UI.
How to fix it?
There are a lot of ways to extract the business logic out of our views. The way to do it will depend on your preferred architecture. If you use MVP, any kind of logic belongs to the Presenter. If you prefer MVVM, your logic should be part of your ViewModel.
At Groupon, we have found that MVI and unidirectional data flows are a great way to fix this problem. The business logic should be part of our Intent, that will produce an immutable state object, that will be rendered by our view.
If you are interested in unidirectional data flows and how to implement reusable UI components. I strongly encourage you to read Pratyush Kshirsagar and Yichen WU article Cross view communication using Grox. They do a great job explaining how unidirectional data flows can help us build our UI.
At this point, you may have realized that we haven’t talked about performance. You might even be surprised that we didn’t even consider performance as one of our principles for good UIs.
While we do believe that performance it is important, readable, reusable and testable code is even more important than optimized code. After all, that’s the reason why we use high-level programming languages instead of writing more efficient assembler code.
In Android, nested layouts and double taxation are two big problems that affect the performance of our UI. Because of that, we are constantly bombarded with articles, podcast and talks telling us to use
ConstrainLayout and avoid nested layouts.
ConstrainLayout is an amazing tool, is more powerful than
RelativeLayout and it doesn’t incur in double taxation. The problem, as usual, happens when we take this to the extreme.
Based on all the articles and talks that we have heard, we may decide that we are going to implement the UI of our activity only using one
Why is this bad?
- By building all our UI in one
ConstraintLayout, we are creating a monolithic UI. Which, as we discussed above, is hard to read, hard to test and produces non-reusable code.
- We are not treating our UI as a first-class citizen. We will never consider building all our business logic in the activity/fragment class. Exactly the same reasons apply to not building our UI one xml file.
How to fix it?
Sacrificing good code for performance is a hard choice and should always be our last resort. To prevent over-optimization we need to make performance tests part of our development process. Our test should tell us when we have a problem and we should only create a monolithic view when no other solution is possible. You will be surprised by how many times UI performance problems are caused by our binding logic doing too much or because our code is refreshing the UI more than needed.
5. Neglecting our UI in code reviews
Reviewing code is hard, time-consuming and to make it worst, xml files are not easy to understand (especially monolithic xmls). For those reasons, we tend to neglect our UI when we review code.
Why is this bad?
- Our app’s UI is its introduction letter to our users. A consistent, clean and well-structured UI can be the difference between our users staying or leaving our app.
- We are not treating our UI as a first-class citizen. Our UI is half of our app, spending time reviewing xmls and views is, at least, as important as reviewing our business logic.
How to fix it?
There are a few things we can do to improve our review process.
As a reviewer:
- It’s perfectly fine to recognize that we are not able to understand an xml. You probably don’t have the right context or the UI is too complex. Ask for help from the author of the code.
- Don’t feel bad about asking the author to break an xml into smaller views. You will do the same with for large class or a large method.
- Start the code review with the UI. As I said, xml files are the hardest part to review, don’t wait to be tired to start reading them.
- Be familiar with the Material Design guidelines. Some easy things I always check are: Do we have a ripple effect as part of our “pressed” state? Do we have/need elevation in our buttons? or Do our animations have the right duration?
As a reviewee:
- Add screenshots of your UI in your Pull Request. This will help your team review the code faster.
- Ask your designer to review your implementation. Your designer is your greatest ally. Ask them to review your UI as early in the development cycle as possible.
- Avoid monolithic xml files. I can’t say this enough, small UI components are better and easier to review.
- Start with your UI. Start any new feature by creating a Pull Request dedicated to your UI. This way you know that your UI will have 100% of the reviewer’s attention.
Building reusable UI components it is not hard but it requires discipline. It requires us to stop thinking in terms of screens and start thinking in terms of components and their relationships.
Reusing our UI help us build new functionalities faster. There is a big difference, in terms of speed and quality, between composing existing UI components and building a complete UI from scratch. Also, reused components contain bug fixes and consider edge cases we may have never thought about.
Summarizing some of the tips that we have discussed:
- Using business models for views is a bad idea. Always decouple your views from your backend and your business logic.
- Avoid monolithic xmls. They make your code hard to read, hard to test and they are the main cause of inconsistencies in an app. Atomic Design can help you break your UI into reusable components.
- Business logic doesn’t belong in the UI. Logging, AB testing or decision making logic don’t belong in a View. Views should only receive an immutable state and render it. Embracing MVI, immutability and unidirectional data flows will help you.
- Optimization on the expense of code quality should be an exception, never a rule. We should only create monolithic views to avoid nested layouts’ penalizations when there are no other options. Build your UI the right way and tackle performance issues only when they actually happen.
- Treat your UI code as a first-class citizen. Start any feature by building its UI first, ask your designer to review it as soon as possible and ask your team to review it independently.
I hope these tips help you with your next Android adventure. With 💚 Carlos Palacin Rubio from the Groupon Android team.