Android Architecture series · Part 3 of 9
MVC and MVP: Taking the Logic Out of the Activity
These two are where most Android apps start. You will see why MVC turns the Activity into a giant file and how MVP gives the logic its own home, with a ViewProxy keeping the view replaceable.
- MVC: simple, but Activity becomes view and controller at once.
- MVP: passive view, presenter holds the logic, model gives data.
- Key trick: the presenter talks to the view through an interface (ViewProxy).
- Cost: boilerplate and manual attach/detach.
1. MVC: the starting point
Model View Controller is the oldest of the family. The Model is the data, the View is what the user sees, and the Controller reacts to the user and updates both.
On Android there is a catch. The system gives you an Activity that already owns the screen, handles clicks and lifecycle. So most “MVC” apps put the view and the controller in the same class:
class ProfileActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profile)
findViewById<Button>(R.id.load).setOnClickListener {
// network + parse + decide + draw, all in the same place
Thread {
val json = URL("https://example.com/profile").readText()
val name = JSONObject(json).getString("name")
runOnUiThread { findViewById<TextView>(R.id.name).text = name }
}.start()
}
}
}
It works, but you cannot unit test it without an emulator, rotation can break the thread, and the file grows forever. People joke that MVC stands for Massive View Controller. This is the pain that MVP, MVVM and the others were invented to fix.
Starting point for a revamp. If your old code looks like this, great: you now know exactly which parts to pull out (network, parsing, decisions) and where they go (Data and Logic boxes).
2. MVP: give the logic its own home
Model View Presenter takes the thinking out of the Activity. The View becomes “passive”: it only draws and forwards clicks. The Presenter decides everything. The Model (repository) provides data.
The contract
MVP usually starts with a “contract” that lists what the view can do and what the presenter can do:
interface ProfileContract {
interface View { // implemented by ViewProxy
fun showLoading()
fun showProfile(profile: Profile)
fun showError(message: String)
}
interface Presenter {
fun attach(view: View)
fun detach()
fun onLoadClicked(id: String)
}
}
The presenter
class ProfilePresenter(
private val repo: ProfileRepository,
private val dispatchers: DispatcherProvider,
) : ProfileContract.Presenter {
private var view: ProfileContract.View? = null
private val scope = CoroutineScope(SupervisorJob() + dispatchers.main)
override fun attach(view: ProfileContract.View) { this.view = view }
override fun detach() { // call from onDestroy to avoid leaks
view = null
scope.coroutineContext.cancelChildren()
}
override fun onLoadClicked(id: String) {
view?.showLoading()
scope.launch {
try {
val profile = withContext(dispatchers.io) { repo.getProfile(id) }
view?.showProfile(profile)
} catch (e: IOException) {
view?.showError("No connection")
}
}
}
}
The view
class ProfileActivity : AppCompatActivity() {
private lateinit var presenter: ProfileContract.Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
presenter = ProfilePresenter(repo = /* injected */, dispatchers = /* injected */)
presenter.attach(ProfileViewProxy(binding))
binding.load.setOnClickListener { presenter.onLoadClicked("42") }
}
override fun onDestroy() {
presenter.detach()
super.onDestroy()
}
}
Notice what changed. The presenter has no Android imports (no Context, no View), so a plain JUnit test can create it with a fake repository and a fake view. The activity shrank to wiring.
Pros and cons
| Good | Watch out |
|---|---|
| Very easy to unit test the presenter. | Boilerplate: a contract and an interface per screen. |
| Clear rule: view is dumb, presenter is smart. | You manage attach and detach yourself. Forgetting detach() leaks the Activity. |
| Works with any UI technology (XML, Compose, even a different platform). | On rotation the presenter is recreated unless you keep it somewhere (Part 7). |
| Great first step when refactoring old MVC code. | Presenters can grow large if you put everything in them. Split with use cases (Part 5). |
3. Refactoring MVC to MVP, step by step
- Create a repository for the network call. Move parsing there.
- Write the
Viewinterface: one method per thing the screen can show. - Move click-handling decisions into a presenter that calls the view interface.
- Make the Activity (or a ViewProxy) implement the view interface.
- Add a unit test for the presenter before you move on to the next screen.
MVP = MVC where the controller got its own class, lost its Android dependencies, and talks to the view only through an interface.