MVP on Android
Thursday, February 18, 2016Before MVP, my Activities have hundreds of lines. Network calls, click handlers, parsing, UI updates, all in one place. Works fine until you need to write a test. Then you realize there is nothing to test because everything is tied to Android.
MVP changes this.
The pattern
Three parts:
View: the Activity or Fragment. Only does UI things. Shows data, handles clicks, nothing else.
Presenter: has the logic. Talks to the Model to get data, tells the View what to display. No Android imports ideally.
Model: data layer. API calls, database, repositories.
The key is that View and Presenter talk through interfaces:
interface LoginView {
fun showLoading()
fun hideLoading()
fun showError(message: String)
fun navigateToHome()
}
interface LoginPresenter {
fun onLoginClicked(email: String, password: String)
fun onDestroy()
}
The Activity implements LoginView. The Presenter implements LoginPresenter and holds a reference to LoginView.
Implementation
class LoginPresenterImpl(
private val authRepository: AuthRepository
) : LoginPresenter {
private var view: LoginView? = null
fun attachView(view: LoginView) {
this.view = view
}
override fun onLoginClicked(email: String, password: String) {
view?.showLoading()
authRepository.login(email, password) { result ->
view?.hideLoading()
if (result.isSuccess) {
view?.navigateToHome()
} else {
view?.showError(result.error)
}
}
}
override fun onDestroy() {
view = null
}
}
The Activity:
class LoginActivity : AppCompatActivity(), LoginView {
private lateinit var presenter: LoginPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter = LoginPresenterImpl(AuthRepository())
(presenter as LoginPresenterImpl).attachView(this)
loginButton.setOnClickListener {
presenter.onLoginClicked(emailInput.text.toString(), passwordInput.text.toString())
}
}
override fun onDestroy() {
super.onDestroy()
presenter.onDestroy()
}
override fun showLoading() { progressBar.visibility = View.VISIBLE }
override fun hideLoading() { progressBar.visibility = View.GONE }
override fun showError(message: String) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() }
override fun navigateToHome() { startActivity(Intent(this, HomeActivity::class.java)) }
}
Why it is good
Testing the Presenter is now easy. No Activity, no Android framework needed:
@Test
fun `when login fails, shows error`() {
val mockView = mock(LoginView::class.java)
val mockRepo = mock(AuthRepository::class.java)
whenever(mockRepo.login(any(), any(), any())).thenAnswer {
(it.arguments[2] as (Result) -> Unit)(Result.failure("wrong password"))
}
val presenter = LoginPresenterImpl(mockRepo)
presenter.attachView(mockView)
presenter.onLoginClicked("test@test.com", "wrong")
verify(mockView).showError("wrong password")
}
Pure JVM test. Fast. No Robolectric, no emulator.
The annoying parts
Alot of interfaces. For every screen you write two interfaces plus the implementation. Small features become alot of files.
Also there is no standard way to handle configuration changes. The Activity gets destroyed on rotation and the Presenter dies too unless you handle it manually (static Presenter map, retained Fragment, etc.).
MVP is a step forward from God Activities. But it is also the beginning of realizing that Android architecture is hard, and everyone has a different solution.