Infrastructure as Code Best Practices with Terraform for DevOps
João Victor Alhadas | Dec 17, 2024
Starting as an Android developer and later working with iOS as well, I had contact with several different projects’ architectures – some good and some bad.
I was happily using the MVP architecture for Android until I met – and worked eight months with – the VIPER architecture in an iOS project. When I came back to Android, I decided to adapt and implement VIPER on it, despite some other devs suggesting it wouldn’t make sense to use an iOS architecture on Android. Given the fundamental difference between Android and iOS’ frameworks, I had some questions about how useful would VIPER be for Android. Would it be doable and worth the effort? Let’s start with the basics.
VIPER is a clean architecture mainly used in iOS app development. It helps keeping the code clean and organized, avoiding the Massive-View-Controller situation.
VIPER stands for View Interactor Presenter Entity Router, which are classes that have a well defined responsibility, following the Single Responsibility Principle. You can read more about it on this excellent article.
There are already some very good architectures for Android. The more famous being Model-View-ViewModel (MVVM) and Model-View-Presenter (MVP).
MVVM makes a lot of sense if you use it alongside data binding, and since I don’t like much the idea of data binding, I’ve always used MVP for the projects I’ve worked on. However, as projects grow, the presenter can become a huge class with a lot of methods, making it hard to maintain and understand. That happens because it is responsible for a lot of stuff: it has to handle UI Events, UI logic, business logic, networking and database queries. That violates the Single Responsibility Principle, something that VIPER can fix.
With those problems in mind, I started a new Android project and decided to use MVP + Interactor (or VIPE, if you will). That allowed me to move some responsibility from the presenter to the Interactor. Leaving the presenter with UI events handling and preparing the data that comes from the Interactor to be displayed on the View. Then, the Interactor is only responsible for the business logic and fetching data from DBs or APIs.
Also, I started to use interfaces for linking the modules together. That way, they can’t access methods other than the ones declared on the interface. This protects the structure and helps defining a clear responsibility for each module, avoiding developer mistakes like putting the logic in the wrong place. Here’s how the interfaces look like:
class LoginContracts {
interface View {
fun goToHomeScreen(user: User)
fun showError(message: String)
}
interface Presenter {
fun onDestroy()
fun onLoginButtonPressed(username: String, password: String)
}
interface Interactor {
fun login(username: String, password: String)
}
interface InteractorOutput {
fun onLoginSuccess(user: User)
fun onLoginError(message: String)
}
}
And here’s some code to illustrate the classes that implement those interfaces (it’s in Kotlin, but Java should be the same).
class LoginActivity: BaseActivity, LoginContracts.View {
var presenter: LoginContracts.Presenter? = LoginPresenter(this)
override fun onCreate() {
//...
loginButton.setOnClickListener { onLoginButtonClicked() }
}
override fun onDestroy() {
presenter?.onDestroy()
presenter = null
super.onDestroy()
}
private fun onLoginButtonClicked() {
presenter?.onLoginButtonClicked(usernameEditText.text, passwordEditText.text)
}
fun goToHomeScreen(user: User) {
val intent = Intent(view, HomeActivity::class.java)
intent.putExtra(Constants.IntentExtras.USER, user)
startActivity(intent)
}
fun showError(message: String) {
//shows the error on a dialog
}
}
class LoginPresenter(var view: LoginContracts.View?): LoginContracts.Presenter, LoginContracts.InteractorOutput {
var interactor: LoginContracts.Interactor? = LoginInteractor(this)
fun onDestroy() {
view = null
interactor = null
}
fun onLoginButtonPressed(username: String, password: String) {
interactor?.login(username, password)
}
fun onLoginSuccess(user: User) {
view?.goToNextScreen(user)
}
fun onLoginError(message: String) {
view?.showError(message)
}
}
class LoginInteractor(var output: LoginContracts.InteractorOutput?): LoginContracts.Interactor {
fun login(username: String, password: String) {
LoginApiManager.login(username, password)
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe({
//does something with the user, like saving it or the token
output?.onLoginSuccess(it)
},
{ output?.onLoginError(it.message ?: "Error!") })
}
}
The full code is available on this Gist.
You can see that the modules are created and linked together on startup. When the Activity is created, it initializes the Presenter, passing itself as the View on the constructor. The Presenter then initializes the Interactor passing itself as the InteractorOutput
.
On an iOS VIPER project this would be handled by the Router, creating the UIViewController
, or getting it from a Storyboard, and then wiring all the modules together. But on Android we don’t create the Activities ourselves: we have to use Intents, and we don’t have access to the newly created Activity from the previous one. This helps preventing memory leaks, but it can be a pain if you just want to pass data to the new module. We also can’t put the Presenter on the Intent’s extras because it would need to be Parcelable
or Serializable
. Is just not doable.
That’s why on this project I’ve omitted the Router. But is that the ideal case?
The above implementation of VIPE solved most of the MVP’s problems, splitting the responsibilities of the Presenter with the Interactor.
However, the View isn’t as passive as the iOS VIPER’s View. It has to handle all the regular View responsability plus routing to other modules. This should NOT be its responsibility and we can do better. Enter the Router.
Here’s the differences between “VIPE” and VIPER:
class LoginContracts {
interface View {
fun showError(message: String)
//fun goToHomeScreen(user: User) //This is no longer a part of the View's responsibilities
}
interface Router {
fun goToHomeScreen(user: User) // Now the router handles it
}
}
class LoginPresenter(var view: LoginContracts.View?): LoginContracts.Presenter, LoginContracts.InteractorOutput {
//now the presenter has a instance of the Router and passes the Activity to it on the constructor
var router: LoginContracts.Router? = LoginRouter(view as? Activity)
//...
fun onLoginSuccess(user: User) {
router?.goToNextScreen(user)
}
//...
}
class LoginRouter(var activity: Activity?): LoginContracts.Router {
fun goToHomeScreen(user: User) {
val intent = Intent(view, HomeActivity::class.java)
intent.putExtra(Constants.IntentExtras.USER, user)
activity?.startActivity(intent)
}
}
Full code available here.
Now we moved the view routing logic to the Router. It only needs an instance of the Activity so it can call the startActivity
method. It still doesn’t wire everything together as the iOS VIPER, but at least it respects the Single Responsibility Principle.
Having developed a project with MVP + Interactor and by helping a coworker to develop a full VIPER Android project, I can safely say that the architecture does work on Android and it’s worth it. The classes become smaller and more maintainable. It also guides the development process, because the architecture makes it clear where the code should be written.
Here on Cheesecake Labs we are planning to use VIPER on most of the new projects, so we can have better maintainability and clearer code. Also, it makes easier to jump from an iOS project to an Android project and vice-versa. Of course this is an evolving adaptation, so nothing here is carved in stone. We gladly appreciate some feedback about it!
A developer who loves to code, learn and build stuff. Also a 3D print enthusiast and full-time nerd.