Still MVP or already MVVM?

It is not so simple to apply and keep clear MVP in Android project. There are many profits from this architecture:

  • Good concerns separation (between presentation and business logic)
  • Presenters are strongly reusable
  • Presenters are easy to unit-test
  • Presenters are independent from Views and Android platform.

The key concept of MVP is that View is hidden behind an interface, and Presenter is applying all changes using its methods. Here is simple example:

interface MainView: PresenterBaseView {
fun showToast(text: String)
}
class MainActivity : BaseActivity(), MainView {
  override fun showToast(text: String) {
toast(text)
}

//...
}
class MainPresenter(val view: MainView) {
  fun onStart() {
view.showToast("I am working")
}
}

To keep it clean, Activity should not depend on Data Model, so View methods should use only basic types (like Int, String) or mapper designed to pass bundle of data (like in Android-CleanArchitecture) The result is that Activity is often full of 3-line methods, that are setting something on layout or checking some properties:

override fun getEmail(): String {
return emailView.text.toString()
}
override fun setEmail(email: String) {
emailView.text = email
}
override fun getPassword(): String {
return passwordView.text.toString()
}
override fun setPassword(password: String) {
passwordView.text = name
}

This is problematic, because:

  • Someone needs to write it all
  • Maintenance of such an amount of code is problematic
  • Boilerplate generates information noise
  • It makes classes looks big and complex while they are doing nearly nothing

One solution to this MVP problem is MVVM architecture — to make bindings between Presenter properties and View traits. This solution is minimalistic and elegant, but for me it is still not mature enough to use it in an important projects.

I feel, that there is no need for magic. While Kotlin introduced property delegation, view bindings can be simply implemented without any annotation processing or any magic proxy. And this idea stands behind KotlinAndroidViewBindings library.

Think about it this way: TextView is element of view, but from the Presenter perspective, it is just field that contains some text. It is because it is the only important property for Presenter. Unless it also wants to change or read something else. Then it looks at View as text and some other property. For example, TextView in XML looks following:

<TextView
android:id="@+id/registerButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp"
android:text="@string/action_go_to_register"
android:textColor="@android:color/white" />

But from Presenter perspective is looks following:

interface MainView {
var text: String
}

We need to also set onClickListener? Then it looks as follow:

interface MainView {
var text: String
var onTextClicked: ()->Unit
}

It would be problematic to implement all this setters and getters, but with KotlinAndroidViewBindings we can use property delegation to make bindings between properties and View traits as simply as possible:

var text by bindToText(R.id.emailView)
var onTextClicked by bindToRequestFocus(R.id.emailView)

Time to some bigger example. It’s simple example from KotlinAndroidViewBindings, and same similar example in wider context can be found on my SimpleKotlinMvpBoilerplate.

For the need of presentation, I implemented login functionality. It is showing different errors and requesting focus if field is incorrect according to validation. It is also showing loading when using repository. All logic is placed on Presenter, which is well unit-tested. View definition is following:

interface LoginView {
var progressVisible: Boolean
var email: String
val emailRequestFocus: ()->Unit
var emailErrorId: Int?
var password: String
val passwordRequestFocus: ()->Unit
var passwordErrorId: Int?
var loginButtonClickedCallback: ()->Unit
fun informAboutLoginSuccess(token: String)
fun informAboutError(error: Throwable)
}

Pretty big, but note that these are minimal capabilities. We just defined quite a complex functionality (we can split it into multiple presenters or views with presenters, but I decided to skip it to keep example more typical).

class LoginActivity : AppCompatActivity(), LoginView {
  override var progressVisible by bindToLoading(R.id.progressView, R.id.loginFormView)
  override var email by bindToTextView(R.id.emailView)
override val emailRequestFocus by bindToRequestFocus(R.id.emailView)
override var emailErrorId by bindToErrorId(R.id.emailView)
  override var password by bindToTextView(R.id.passwordView)
override val passwordRequestFocus by bindToRequestFocus(R.id.passwordView)
override var passwordErrorId by bindToErrorId(R.id.passwordView)
  override var loginButtonClickedCallback by bindToClick(R.id.loginButton)
  val presenter by lazy { LoginPresenter(this) }
  override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
presenter.onCreate()
}
  override fun onDestroy() {
super.onDestroy()
presenter.onDestroy()
}
  override fun informAboutLoginSuccess(token: String) {
toast("Login succeed. Token: $token")
}
  override fun informAboutError(error: Throwable) {
toast("Error: " + error.message)
}
}

And Presenter:

class LoginPresenter(val view: LoginView) {
  val loginUseCase by lazy { LoginUseCase() }
val validateLoginFieldsUseCase by lazy { ValidateLoginFieldsUseCase() }
var subscriptions: List<Subscription> = emptyList()
  fun onCreate() {
view.loginButtonClickedCallback = { attemptLogin() }
}
  fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}
  fun attemptLogin() {
val (email, password) = view.email to view.password
subscriptions += validateLoginFieldsUseCase
.validateLogin(email, password)
.smartSubscribe(
onSuccess = { (emailErrorId, passwordErrorId) ->
view.passwordErrorId = passwordErrorId
view.emailErrorId = emailErrorId
when {
emailErrorId != null ->
view.emailRequestFocus()
passwordErrorId != null ->
view.passwordRequestFocus()
else -> sendLoginRequest(email, password)
}
},
onError = view::informAboutError
)
}
  private fun sendLoginRequest(email: String, password: String) {
loginUseCase.sendLoginRequest(email, password)
.applySchedulers()
.smartSubscribe(
onStart = { view.progressVisible = true },
onSuccess = { (token) ->
view.informAboutLoginSuccess(token)
},
onError = view::informAboutError,
onFinish = { view.progressVisible = false }
)
}
}

And it is all easy to unit-test with mocked View: (full tests here)

@Test
fun checkBothLoginFieldsEmpty() {
val mockedView = MockedLoginView()
val presenter = LoginPresenter(mockedView)
presenter.onCreate()
mockedView.loginButtonClickedCallback.invoke()
checkValidity(mockedView,
expectedEmailError = R.string.error_field_required,
expectedPasswordError = R.string.error_field_required
)
}

But what about initial question? Still MVP or already MVVM? Well, I am not sure. Too much philosophy. I prefer programming. And it is definitely useful ;)


Originally published at marcinmoskala.com on May 5, 2017.

To stay up-to-date with other articles, just follow this medium or observe me on Twitter. If you need some help then remember that I am open for consultations.

If you like it, remember to clap. Note that if you hold the clap button, you can leave more claps.