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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started with MVP (Model View Presenter) on Android
25 mins
- Getting Started
- MVC: The Traditional Approach
- What Is Wrong With This Approach?
- MVP: An Alternative Approach
- Refactoring Umbrella
- Organizing Features
- Adding Dependency Injection
- Defining the Contract
- Defining the Presenter
- Writing the View
- Testing the Presenter
- Pitfalls of MVP
- View Lifecycle
- Presenter Code Reuse
- Presenter State
- Where to Go From Here?
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.
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.
To learn more, read the tutorials Dependency Injection in Android with Dagger 2 and Kotlin or Dependency Injection with Koin.
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:
- 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.
- The presenter holds on to an instance of the
WeatherRepository
, which in this app is the model. - 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
:
- Implement the
MainContract.View
interface. This jives well with our expectations of views. - Add a
presenter
property instead of the modelweatherRepository
. As was previously mentioned, the view needs the presenter to invoke user initiated callbacks. - 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. - Offload handling of the button callback to the presenter.
- 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.
- Implement the method required from the
BaseView
interface to set the presenter. - Add
override
to thedisplayWeatherState()
method, since it is now part of the view interface.
Build and run the app just to make sure it all still works.
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.