Getting Started with MVP (Model View Presenter) on Android

In this hands-on tutorial, we apply a design pattern called MVP, short for Model-View-Presenter, to an Android application. By Jinn Kim.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Refactoring Umbrella

You are going to transition the Umbrella app to an MVP architecture. Here’s a diagram of the components that you will add to the project, and how each component will interact with each other.

Android Sample App - Model View Presenter

Organizing Features

One popular way of managing the parts of an app is to organize them by feature. A feature is composed of the model, the views, the presenters, as well as dependency injection (DI) code to create and provide each component. This way, you can add and remove features from your app as a module.

Your app has only one feature: the main screen. You are going to implement MVP for the main screen, and you will create some components prefixed with Main.

Adding Dependency Injection

In this section, you will create a handmade dependency injector.

To start, create an interface named DependencyInjector in the root com.raywenderlich.android.rwandroidtutorial/ package.

interface DependencyInjector {
​  fun weatherRepository() : WeatherRepository
}

Next, create a class DependencyInjectorImpl in the same package that implements the interface.

class DependencyInjectorImpl : DependencyInjector {
  override fun weatherRepository() : WeatherRepository {
​    return WeatherRepositoryImpl()
  }
}

There’s no strict reason here for why we split this dependency injector class into an interface and an implementation class, however, it’s just considered good practice in case you ever wanted to swap in a different implementation in the future.

To learn more, read the tutorials Dependency Injection in Android with Dagger 2 and Kotlin or Dependency Injection with Koin.

Note: In a production app, you will choose among any of the DI frameworks in the Android ecosystem (e.g., Dagger2), which will help in working with an MVP-architected app.

Defining the Contract

We also have interfaces to define the presenter and the view. Interfaces help with decoupling the parts of the app. The interface forms a contract between the presenter and view.

First, create a new file named BasePresenter.kt in the same base package you’ve been working in, and add the following code:

interface BasePresenter {
​  fun onDestroy()
}

This is a generic interface that any presenter you add to your project should implement. It contains a single method named onDestroy() that basically acts as a facade for the Android lifecycle callback.

Also, create a new file named BaseView.kt, and add the following code:

interface BaseView<T> {
  fun setPresenter(presenter : T)
}

Similar to BasePresenter, this is the interface that all views in your app should implement. Since all views interact with a presenter, the view is given a generic type T for the presenter, and they must all contain a setPresenter() method.

Next, create a contract interface named MainContract, which defines interfaces for the view and presenter for the Main screen, and update it to look as follows:

interface MainContract {
  interface Presenter : BasePresenter {
​    fun onViewCreated()
​    fun onLoadWeatherTapped()
  }

  interface View : BaseView<Presenter> {
​    fun displayWeatherState(weatherState: WeatherState)
  }
}

Notice here that you’re creating interfaces for the specific activity, and that they inherit from the the base interfaces we previously defined. You can see that MainContract.Presenter is interested in being called back by the MainContract.View when the view is created through onViewCreated() and when the user taps on the “Load Weather” button through onLoadWeatherTapped(). Similarly, the view can be invoked to display weather information through displayWeatherState(), which is only called by the presenter.

Defining the Presenter

You have your interfaces in place. Now it’s a matter of assigning these responsibilities to the proper class. First, create a new file named MainPresenter.kt, and set it up as follows:

// 1
class MainPresenter(view: MainContract.View,
                    dependencyInjector: DependencyInjector)
  : MainContract.Presenter {
  // 2
  private val weatherRepository: WeatherRepository
      = dependencyInjector.weatherRepository()

  // 3
  private var view: MainContract.View? = view
}

Taking this code in pieces:

  1. The presenter constructor takes in an instance of the view, along with the dependency injector created earlier, which it uses to get an instance of the model.
  2. The presenter holds on to an instance of the WeatherRepository, which in this app is the model.
  3. The presenter also holds on to a reference to the view; however, note that it interacts with the interface only, as defined in MainContract.

Next, move two private methods from MainActivity into the presenter.

private fun loadWeather() {
  val weather = weatherRepository.loadWeather()
  val weatherState = weatherStateForWeather(weather)

  // Make sure to call the displayWeatherState on the view
  view?.displayWeatherState(weatherState)
}

private fun weatherStateForWeather(weather: Weather) : WeatherState {
​  if (weather.rain!!.amount!! > 0) {
​    return WeatherState.RAIN
​  }
​  return WeatherState.SUN
}

There’s nothing remarkable about these methods, however, be sure to forward the call to displayWeatherState() in loadWeather() to your view object:

view?.displayWeatherState(weatherState)  

Finally, implement the rest of the presenter contract by adding the following methods:

override fun onDestroy() {
  this.view = null
}

override fun onViewCreated() {
​  loadWeather()
}

override fun onLoadWeatherTapped() {
​  loadWeather()
}

Here, you do some clean up in onDestroy() and invoke fetching the weather data in both onViewCreated() and onLoadWeatherTapped().

An important point to notice is that the presenter has no code that uses the Android APIs.

Writing the View

Now, replace the content of MainActivity.kt with the following:

package com.raywenderlich.android.rwandroidtutorial

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.Button
import android.widget.ImageView

// 1
class MainActivity : AppCompatActivity(), MainContract.View {
  internal lateinit var imageView: ImageView
  internal lateinit var button: Button

  // 2
  internal lateinit var presenter: MainContract.Presenter

  override fun onCreate(savedInstanceState: Bundle?) {
​    super.onCreate(savedInstanceState)
​    setContentView(R.layout.activity_main)

    imageView = findViewById(R.id.imageView)
    button = findViewById(R.id.button)
    
    // 3
    setPresenter(MainPresenter(this, DependencyInjectorImpl()))
    presenter.onViewCreated()
    
    // 4
    button.setOnClickListener { presenter.onLoadWeatherTapped() }
  }

  // 5
  override fun onDestroy() {
​    presenter.onDestroy()
​    super.onDestroy()
  }

  // 6
  override fun setPresenter(presenter: MainContract.Presenter) {
​    this.presenter = presenter
  }

  // 7
  override fun displayWeatherState(weatherState: WeatherState) {
​    val drawable = resources.getDrawable(weatherDrawableResId(weatherState),
​            applicationContext.getTheme())
​    this.imageView.setImageDrawable(drawable)
  }

  fun weatherDrawableResId(weatherState: WeatherState) : Int {
​    return when (weatherState) {
​      WeatherState.SUN -> R.drawable.ic_sun
​      WeatherState.RAIN -> R.drawable.ic_umbrella
​    }
  }
}

Let’s highlight the most important changes made to MainActivity:

  1. Implement the MainContract.View interface. This jives well with our expectations of views.
  2. Add a presenter property instead of the model weatherRepository. As was previously mentioned, the view needs the presenter to invoke user initiated callbacks.
  3. Store a reference to the presenter just after creating it. Notice that it also creates and passes an instance of DependencyInjectorImpl as part of the creation.
  4. Offload handling of the button callback to the presenter.
  5. Notify the presenter when the view is being destroyed. Recall that the presenter uses this opportunity to clean up any state that is no longer required beyond this point.
  6. Implement the method required from the BaseView interface to set the presenter.
  7. Add override to the displayWeatherState() method, since it is now part of the view interface.

Build and run the app just to make sure it all still works.

Screenshot of MVP sample app

Based on the refactoring you’ve done to MainActivity, it should be clear how the app data flows and how the plumbing is set up between the model, the view and the presenter.

Congratulations! You’ve MVP-ified your app with a different architecture. You’ve managed to extract all the business logic to a place where it can now be easily tested with lightweight unit tests. You’ll tackle that in the next section.