Wassim Beltaief

MVP on Android

Thursday, February 18, 2016

Before 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.