Poisoning Android views with logic

Cem Tüver
Onfido Product and Tech
6 min readOct 13, 2022

Android development has been evolving non-stop since its initial release. At first, there were Activities containing nearly all of the code. Then the community started implementing high-level architectural patterns such as MVC and MVP. Today, we have Fragments, ViewModel, AndroidX, Kotlin, MVVM, and countless patterns, frameworks and libraries to help us build apps. However, no matter how excellent and fault-proof your chosen Android app architecture is, it is still challenging to do everything right.

This blog post demonstrates an example of small decisions that can break the whole architecture. It shows how poorly named functions end up poisoning Android views with logic by discussing an implementation of a login screen with MVP architecture. Yet, the main idea is independent of architectural patterns, and you can apply your learnings to any of them, such as MVC, MVVM, etc.

Sample login screen

Screenshot of the login page of an Android app with two input fields, e-mail and password, and one button with the text “Login”.

The screen expects the user to type their e-mail and password; then, it validates them. If the user types a valid e-mail and password combination, the login screen lets them view the page content. Otherwise, it displays an error. It sounds pretty straightforward, yet the implementation is complicated. So let’s break this login flow into steps and then implement it.

  1. The user types their e-mail and password.
  2. The user clicks the “Login” button.
  3. The app displays a loading indicator and asks an authority whether the e-mail and the password are correct. Most of the time, this authority is a backend service.
  4. The authority responds if the e-mail and password combination matches with a valid user account.
  5. The app dismisses the loading indicator, and depending on the authority’s response, it lets the user view the page content or shows an error.

If we dive into the details and divide “the app” into model, view and presenter layers, we would have the following flow.

  1. The user types their e-mail and password on the view.
  2. The user clicks the “Login” button on the view.
  3. The view shows a loading indicator, and the presenter sends the e-mail and password to the model layer to validate them.
  4. The model layer transmits the e-mail and the password to the authority, and delivers the authority’s response back to the presenter.
  5. After the presenter receives the response, the view dismisses the loading indicator. It then displays the content if the response indicates that the user has provided a valid e-mail and password combination. Otherwise, it shows an error.

What might go wrong?

As you see, each layer has its own responsibilities and performs different sets of operations. Even though their implementations are essential for an ideal architecture, the communication between them also plays a vital role. For example, after the user clicks the “Login” button, the view gets notified from the Android SDK Button. Then it transmits that event to the presenter layer, triggering the login flow.

Like most architectural patterns, MVP defines the way of communication between layers. For instance, it uses invoke-based communication between the view and the presenter layers. However, implementation depends on the developer, and things might go wrong here. So let’s look at the following activity, a possible implementation of the login screen using MVP.

class LoginActivity : Activity() {    override fun onCreate(savedInstanceState: Bundle?) {
...
loginButton.setOnClickListener {
presenter.login(
emailEditText.text,
passwordEditText.text
)
}
}
}

And here is the presenter:

class LoginPresenter {    fun login(email: String, password: String) {
view.showLoadingIndicator()
loginUseCase.execute(
email,
password,
onResult = { result ->
view.hideLoadingIndicator()

if (result is Error) view.showError(result.error)
else if (result is Success) view.showContent()
}
)
}
}

Do you see any issue related to the communication between the activity and the presenter? The activity calls presenter.login function and triggers the login flow as soon as the user clicks the “Login” button. Keeping this in mind, which one relates the button click event with the login flow initiation, the activity or the presenter? If you say the activity, you are right. And this is the first stage of poisoning Android views with logic: letting the view control the flow.

Assume that you are required to add a feature to the login screen. For example, your company now wants to let the users use the app as guests without logging in. You need to add a “Continue as a guest” button and clicking that button would log in the user with an empty e-mail and an empty password. Additionally, the app should track the clicks on the “Continue as a guest” button to analyse the new feature’s usage.

Screenshot of the updated login page with an additional button, “Continue as a guest”.

One of the possible implementations is that the activity tracks the user behaviour and calls presenter.login function when the user clicks the “Continue as a guest” button. Like below:

class LoginActivity : Activity() {    override fun onCreate(savedInstanceState: Bundle?) {
...
guestButton.setOnClickListener {
trackGuestButtonClick()
presenter.login(
email = "",
password = ""
)
}
}
}

It would work, but unfortunately, the activity now contains business logic. It knows that the clicks on the “Continue as a guest” button should be tracked and the user should be logged in with an empty e-mail and an empty password. This is the second stage of poisoning Android views with logic: executing business logic.

Of course, you can define another login function in the presenter that tracks the click event and logins the user as a guest. Then you can call it from the activity. Such as:

class LoginPresenter {    fun trackAndContinueAsGuest() {
trackGuestButtonClick()
login(email = "", password = "")
}
fun login(email: String, password: String) {
...
}
}

Unfortunately, this version is only slightly better. Even though the presenter calls the login function and executes the business logic, the activity still decides when to call which function. The developer must still think about the business logic while coding the activity, as there is no written contract between the activity and the presenter. They need to remember to call presenter.trackAndContinueAsGuest function when the user clicks the “Continue as a guest” button, and presenter.login function when the user clicks the “Login” button. Moreover, nothing stops them calling login function with empty parameters when the user clicks the “Continue as a guest” button. This is the third and the last stage of poisoning Android views with logic: hidden contracts.

Solution: logic-free contract

Let’s list all the requirements first.

  1. The view shouldn’t control the flow.
  2. The view shouldn’t execute any business logic.
  3. There should be a written contract between the view and the presenter.

Basically, the view should only listen the required events and informs the presenter when they happen. Then the presenter should control the flow and execute the business logic.

Let’s update the login activity and the presenter according to these requirements. The activity should transmit the “Continue as a guest” button click events and the “Login” button click events to the presenter. If we define a contract for those events, we will end up having an activity like below:

class LoginActivity : Activity() {    override fun onCreate(savedInstanceState: Bundle?) {
...
loginButton.setOnClickListener {
presenter.onLoginButtonClick(
emailEditText.text,
passwordEditText.text
)
}
guestButton.setOnClickListener {
presenter.onGuestButtonClick()
}
}
}

And the following presenter:

class LoginPresenter {    fun onLoginButtonClick(email: String, password: String) {
login(email, password)
}
fun onGuestButtonClick() {
trackGuestButtonClick()
login(email = "", password = "")
}
private fun login(email: String, password: String) {
...
}
}

In the end, the activity doesn’t control flow or execute business logic. So even though it still calls the presenter’s functions, the contract is logic-free, and the developer doesn’t have to consider any logic while coding the activity. It is also safer to make changes on the presenter as the developer can quickly identify the use cases by looking at the function names.

--

--