Confession: I have a long history of ignoring most architectural patterns in my Android codebase. I cast them aside very quickly. It all just seemed too complex, and for what? I know how to organize my code! I don't need this "arbitrary" separation-thingy! And testing? Pft. Please.
It wasn't until I got to my current position that I began to understand it a bit more. Though -- another confession -- it wasn't because I jumped at the chance to change my mind: I didn't have a choice.
I joined a small Android team with just one other developer. They were the sole developer before me. They were handed a codebase that needed some love and attention. They took that code and began modernizing it, implementing some structure to the codebase. Then I came along.
Those patterns I'd been ignoring for years? All there. MVP, unit testing, Dagger. -shudder- But I was eager to learn and maybe now I'd finally see the benefit of it all. It took some time and there was definitely a learning curve to it. (Especially Dagger! -shudder-) But the more I used everything, the more I saw some of these benefits that escaped me in the beginning.
What exactly is MVP?
MVP stands for Model-View-Presenter. You've probably come across similar acronyms such as MVVM (Model-View-ViewModel) and MVC (Model-View-Controller). They're all ways of organizing code and enhancing testability. Each is similar in a lot of ways but different in others; one style of organization doesn't fit all, so you'll need to experiment and see which style works best for your project. If MVP had not already been established in this codebase, I would be using MVVM, using Google's new ViewModel class.
On the surface, MVP and the other architectures seem like a lot of extra work, which was my primary reason for initially passing it by. Why do I need to separate my code when I can just shove it all into one class? 😅 I mean, come on, I can just name functions properly, right? I'll keep it clean and organized, I promise.
LMAO.
I can't imagine going back to anything less. It makes my classes smaller, improves testability which tends to improve my actual codebase, and leaves my code more organized. I've seen some of my old code. It could have desperately used some structure.
The Model
This is probably the part you're most familiar with. This is the data that you're acting on. In a Calculator app, this would be the current input. In a Weather app, this would be the current location, the current weather, and maybe the upcoming conditions as well. Some business logic will go here occasionally, but rarely, and not much. I try to keep them as simple as possible.
For example, let's make a model called ClickAmount
.
data class ClickAmount(var numberOfTimesClicked: Int)
Easy, right? This data class will track the number of times a button was clicked. We'll use that data to help control what happens in our view.
The View
This needs to be done in two parts: first the interface, then the actual representation. In MVP, you need to make an interface for your View with functions that alter the view. This interface will be implemented by your representation. In Android, this is normally going to be your Activities or Fragments.
The Interface
interface ButtonClickView {
fun changeBackgroundToRed()
fun changeBackgroundToGreen()
fun updateNumberOfTimesClicked(number: Int)
fun showSuccessMessage()
fun showMinusButton(show: Boolean)
}
This interface contains a few functions for our sample. There's two for changing the background color, updating a TextView with the current number (though our interface and Presenter don't know it's a TextView; it could be anything), showing a success message, and showing or hiding another button. This interface is very useful later. When you test your Presenter in the future you won't need to call every implementation of this interface, just verify()
that the function on the interface was called.
The Implementation
class ButtonClickActivity: Activity(), ButtonClickView {
@BindView(R.id.background) private lateinit var background: View
@BindView(R.id.number_view) private lateinit var numberOfTimesClickView: TextView
@BindView(R.id.plus_button) private lateinit var plusButton: Button
@BindView(R.id.minus_button) private lateinit var minusButton: Button
// In a real application, this would be injected.
private lateinit var presenter: ButtonClickPresenter
// One, two, skip a few...
override fun changeBackgroundToRed() {
background.setBackgroundColor(Color.RED)
}
override fun changeBackgroundToGreen() {
background.setBackgroundColor(resources.getColor(R.color.green))
}
override fun updateNumberOfTimesClicked(number: Int) {
numberOfTimesClickView.text = resources.getString(R.string.number_of_times_format, number)
}
override fun showSuccessMessage() {
Snackbar.make(background, R.string.success, Snackbar.LENGTH_SHORT).show()
}
override fun showMinusButton(show: Boolean) {
minusButton.visibility = if (show) View.VISIBLE else View.GONE
}
@OnClick(R.id.plus_button)
fun onPlusButtonClicked() {
presenter.onPlusButtonClicked()
}
}
Here we implement the interface: changing background colors, updating a TextView, showing a Snackbar, and altering the visibility of a button. We also see a function for when the plus button is clicked. When that happens, we tell the Presenter that it happened, and we leave whatever comes next to the Presenter. The Presenter is free to use any of the methods in the View's interface.
The Presenter
This is where all of your magic happens. And by magic, I mean business logic. Things like network and database calls, sorting and filtering, and complex logic. In MVP, you want your views to just listen to commands, all of which should come from the Presenter. It seems redundant and perhaps a bit circular at first. A button is clicked in your view (onPlusButtonClicked
), so the view tells the Presenter, and all the Presenter might do is showMinusButton()
.
class ButtonClickPresenter() {
// In a real application, this wouldn't be like this either.
private lateinit var view: ButtonClickView
// Three, four, skipping more...
var count = ClickAmount()
fun onPlusButtonClicked() {
count.numberOfTimesClicked++
getView().showMinusButton(count > 0)
alterBackgroundColor()
}
fun onMinusButtonClicked() {
count.numberOfTimesClicked--
getView().showMinusButton(count > 0)
alterBackgroundColor()
}
private fun alterBackgroundColor() {
if (count.numberOfTimesClicked % 2 == 0) {
getView().changeBackgroundToRed()
} else {
getView().changeBackgroundToGreen()
}
}
}
The Reason
"But, Tavon," you ask, "why not just have the view call the methods within itself?"
Splitting our code like this does several things. The View should not know what happens when anything is acted on. The View should simply alert the Presenter, which will handle all of the logic that takes place after being alerted. The Presenter will then tell the View what it should look like or do based on any additional operations or logic.
Another good reason (though it hasn't happened for me yet) is the ability to reuse Views and Presenters. I ran across an article a while back explaining how to use Kotlin across different platforms, namely iOS. With my codebase set up using MVP, I can keep theoretically keep my Presenter in Kotlin and then implement the View in iOS. My logic remains untouched and shared across platforms while my View would look native.
And, also, it's just easier to navigate. I know that when I'm going to alter logic it'll be in the Presenter. Changing the color of the button? The View.
Android Specific Tip: After learning and getting used to MVP, I began to realize more and more that, in Android, the Presenter should not even know that Android exists. Meaning, no Contexts, no Resources, no Activities, etc. This means I put all of that inside of my View. In my Views, I tend to have methods like navigateToSomeScreen()
that will perform the Activity switching because my Presenter doesn't know what an Activity or Context is. Same with switching Fragments. Also, I use String resources (resources.getString()
) in my View as well, meaning that my methods are normally called something like renderThisScreenTitle()
. This keeps all of my Android logic in my View. This does tend to lead to some oddities occasionally, though I tend to solve this by passing whatever dependency needs Android-specific things into the View and have the View act on that, while keeping the dependency in the Presenter, unaware.
The Takeaway
MVP and similar patterns aren't as complicated as they initially appear. They provide much-needed structure in a lawless land and helps improve code quality and testability.
Any questions or comments? Drop me a comment below or contact me on Twitter @gatlingxyz.
Let's Code: Model-View-Presenter
Breaking down what MVP (Model-View-Presenter) is and what it can do for your applications.