Chapters

Hide chapters

Advanced Android App Architecture

First Edition · Android 9 · Kotlin 1.3 · Android Studio 3.2

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

8. MVP Sample
Written by Yun Cheng

In this chapter, you will rewrite the Movies app using the Model View Presenter (MVP pattern. Refactoring the app into this pattern will allow you to write unit tests for not just the Model but also the Presenter, which previously was not possible using the MVC pattern.

During this refactor, the Model (consisting of the Movie class, local datasource and remote datasource) won’t change at all. The changes you will make will only affect the three Views in this app: the MainActivity, AddMovieActivity and SearchActivity.

Getting started

Before you start coding, you will reorganize your project folder structure to group together classes for each screen of the app. Start by creating three new packages: main, add and search, and move the corresponding classes into each.

Before and after directory structure.
Before and after directory structure.

Applying MVP to the Movies app

For each of the three screens, you need to do the following:

  1. Create a new Presenter class and connect it to the View and Model via dependency injection.
  2. Create a new Contract class that contains the PresenterInterface and ViewInterface.
  3. Move all presentation logic into the Presenter.

Moving the presentation logic out of the View and into the Presenter class will achieve a greater degree of separation of concerns. You will reduce the role of the View to be that of displaying the data, listening for user input and handling navigation. Any logic that does not fall under one of those categories should be moved out into the Presenter so that the logic can be unit tested.

For each of the Views, you will also create a corresponding Contract class that contains interfaces for the Presenter and View. It is necessary for the Presenter to interact with an interface of the View because you must keep the Presenter free of any Android framework-specific classes if you wish to write unit tests on the Presenter.

Although the Presenter doesn’t strictly need an interface, you will also create one for it anyway and treat the Contract class as a document that describes communication between the View and Presenter.

The Main screen

Recall that the main screen of the app displays the user’s list of movies to watch, with a Delete icon in the toolbar and an Add floating action button. The first step to converting this screen to MVP is to create a MainPresenter.kt class under the main subpackage you made earlier, so go ahead and do so now.

Note: Whenever you add code, make sure to import the appropriate packages by pressing Alt+Enter on Windows or Option+Enter on Mac.

class MainPresenter(private var view: MainActivity, private var dataSource: LocalDataSource) {  

  private val TAG = "MainPresenter"
  ...
}

This Presenter will need to interact with both the View and the Model, so the MainActivity and the LocalDataSource must be passed into the constructor. As you learned in Chapter 5: “Dependency Injection,” it is important to inject dependencies like the View and the Model in through the constructor for unit testing purposes. When you write unit tests for MainPresenter you will pass in mock objects for the View and the Model; this way you will unit test the MainPresenter class itself and not its dependencies.

Now, create a MainContract.kt class under the main subpackage:

class MainContract {

  interface PresenterInterface {  
    //TODO: add interface methods for Presenter
  }  

  interface ViewInterface {  
    //TODO: add interface methods for View
  }  
}

For now, this class will contain empty definitions for a PresenterInterface and a ViewInterface. You will add methods to these interfaces later, as you build out the Presenter.

Now that you have the interfaces set up for the View and the Presenter, open MainActivity.kt and update the MainActivity to implement the MainContract.ViewInterface like the following:

class MainActivity : AppCompatActivity(), MainContract.ViewInterface {
  ...
}

Next, open MainPresenter.kt and update the MainPresenter class so that it implements the MainContract.PresenterInterface and also holds a reference to a MainContract.ViewInterface for its View rather than a reference to the direct MainActivity implementation:

class MainPresenter(
    private var viewInterface: MainContract.ViewInterface,
    private var dataSource: LocalDataSource) : MainContract.PresenterInterface {  

  private val TAG = "MainPresenter"
  ...
}

Throughout this Presenter class, the Presenter will interact with the MainContract.ViewInterface rather than the MainActivity implementation directly. This is done to avoid having Android framework-specific classes like Activity in the Presenter. MainActivity extends AppCompatActivity, which is specific to the Android framework and cannot be mocked, making it difficult to write unit tests.

Now that you have created the MainPresenter class, you need to instantiate the MainPresenter inside the MainActivity.

Open MainActivity.kt again, and in the MainActivity class, add the following code:

private lateinit var mainPresenter: MainContract.PresenterInterface

private fun setupPresenter() {  
  val dataSource = LocalDataSource(application)  
  mainPresenter = MainPresenter(this, dataSource)  
}

Here, you instantiate the MainPresenter, pass in the Activity itself using the this keyword and a local instance of the LocalDataSource.

Next add a call to setupPresenter() inside the MainActivity’s onCreate() method:

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

Great! You’ve wired up a basic Presenter, but it doesn’t really do much yet. In the next section, you’ll add code for the Presenter to retrieve movies.

Fetching movies

Now that you have connected the Presenter and View, you can begin to move the presentation logic out of the View and into the Presenter. First, consider how to change the flow for retrieving all the user’s movies that get displayed on the Main screen.

Instead of having the View (Activity) perform both the retrieving and displaying of the movies, you can move the retrieving logic into the Presenter.

The following diagram breaks down the sequence of steps.

Flow for retrieving movies.
Flow for retrieving movies.

You can see that this is a three step process:

  • Step One: The view asks the Presenter to get the movie list.
  • Step Two: The Presenter gets the movie list from the Model. The Model returns a list of Movie objects to the Presenter or an error in the event that the Model cannot return the list.
  • Step Three: The Presenter then uses the ViewInterface to tell the View how to display the error. If the Presenter receives an error from the Model, it will tell the ViewInterface to present an error state to the user.

Step One delegates the responsibility of retrieving movies the Presenter, so start by opening MainContract.kt and adding the getMyMoviesList() method to the Presenter’s interface as follows:

  interface PresenterInterface {
    fun getMyMoviesList()  
  }

Then, implement Step One of the flow by opening MainActivity.kt and replacing the contents of the onStart() method of MainActivity as follows:

override fun onStart() {  
  super.onStart()  
  mainPresenter.getMyMoviesList()  
}

As soon as the View starts, it asks the Presenter to retrieve the list of movies.

Next, you’ll implement Step Two and Step Three of the flow, getting and displaying the list of movies.

Open MainPresenter.kt and add the following code, moved over from the MainActivity:

private val compositeDisposable = CompositeDisposable()

//1
val myMoviesObservable: Observable<List<Movie>>
  get() = dataSource.allMovies

//2
val observer: DisposableObserver<List<Movie>>
  get() = object : DisposableObserver<List<Movie>>() {

    override fun onNext(movieList: List<Movie>) {
      if (movieList == null || movieList.size == 0) {
        viewInterface.displayNoMovies()
      } else {
        viewInterface.displayMovies(movieList)
      }
    }

    override fun onError(@NonNull e: Throwable) {
      Log.d(TAG, "Error fetching movie list.", e)
      viewInterface.displayError("Error fetching movie list.")
    }

    override fun onComplete() {
      Log.d(TAG, "Completed")
    }
  }

//3
override fun getMyMoviesList() {
  val myMoviesDisposable = myMoviesObservable
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribeWith(observer)

  compositeDisposable.add(myMoviesDisposable)
}

That’s a lot of code, so let’s go through it one step at a time:

  1. The myMoviesObservable is an observable for the list of Movie objects from the dataSource. This code is the same as before, except now the Presenter will be the one interacting with the dataSource to get movies, which is why you passed in the dataSource to the Presenter through the constructor.
  2. Inside the observer, the Presenter determines how to consume the list of movies it receives, depending on the size of the list and whether an error has occurred. What has changed in this code is that we must interact with the View by calling methods on the viewInterface property to respond to each of these scenarios. The Presenter does not know how to display lists or errors, so it delegates those tasks to the View. If there are no movies to display, the Presenter asks the View to display no movies. If there are movies to display, the Presenter asks the View to display the list of movies.
  3. The getMyMoviesList() method connects the observer to the myMoviesObservable so that it can begin observing. The method is exactly the same as before, except now it belongs to the Presenter.

As you can see, in Step Two of the retrieval flow occurs in getMyMoviesList(), where the Presenter gets the list of movies from the Model by subscribing to an observable of the LocalDataSource’s list of allMovies. Although the Presenter contains the logic that decides what to display, the actual job of displaying content belongs to the View.

As part of Step Three in the flow, the Presenter will ask the View to display the list of movies; otherwise, it will display a view that indicates that there are no movies to display or else display an error message if an error occurred.

Next, the code inside the MainActivity will need to be slightly refactored to handle these scenarios. But, first, you need to define the View interface.

Open MainContract.kt and add the following methods to MainContract.ViewInterface:

interface ViewInterface {
  fun displayMovies(movieList: List<Movie>)
  fun displayNoMovies()
  fun displayMessage(message: String)
  fun displayError(message: String)
}

Now, open up MainActivity.kt again, remove the existing implementation of displayMovies(), and add the following methods:

//1
override fun displayMovies(movieList: List<Movie>) {
  adapter.movieList = movieList
  adapter.notifyDataSetChanged()

  moviesRecyclerView.visibility = VISIBLE
  noMoviesTextView.visibility = INVISIBLE
}

//2
override fun displayNoMovies() {
  Log.d(TAG, "No movies to display.")

  moviesRecyclerView.visibility = INVISIBLE
  noMoviesTextView.visibility = VISIBLE
}

Let’s take the above code in turn:

  1. In this scenario in which there is a list of movies to display, the adapter’s movieList gets updated to the list passed in, and the adapter calls notifyDataSetChanged() to update the data. The RecyclerView is shown and the TextView hidden.
  2. When there are no movies to display, the method hides the RecyclerView and shows a TextView indicating to the user that there are no movies to display.

Also notice that the MainAdapter remains an instance variable of the MainActivity.

By definition, the Adapter class serves as a bridge between the view and the data source, so it is somewhat unclear whether the Adapter belongs to the View or Presenter. But because the RecyclerView undoubtedly belongs in the View, and a RecyclerView requires an associated Adapter, you will keep the Adapter inside the View.

Next, although the MainActivity already contains the methods displayMessage(message: String) and displayError(message: String), you want to override them as part of the ViewInterface methods. This way, the Presenter will be able to call those methods on its instance of ViewInterface, such as when it calls viewInterface.displayError("Error fetching movies") when it fails to retrieve any movies.

Inside MainActivity, simply prepend the override keyword to each of the two methods like so:

override fun displayMessage(message: String) {
  Toast.makeText(this@MainActivity, string, Toast.LENGTH_LONG).show()
}

override fun displayError(message: String) {
  displayMessage(message)
}

You’re nearly done, but before we can move on, we should implement a means to clean up our Observable subscriptions. Even though the compositeDisposable now lives in the Presenter rather than the Activity, you will still need to clear the CompositeDisposable when the Activity is stopped. In the Activity’s onStop(), be sure to tell the Presenter to do that work by changing that method to look like this:

override fun onStop() {
  super.onStop()
  mainPresenter.stop()
}

Now, open MainContract.kt and add the stop() method to the PresenterInterface as follows:

interface PresenterInterface {
  ...
  fun stop()
}

Then open MainPresenter.kt and implement the method like this:

override fun stop() {  
  compositeDisposable.clear()  
}

This method clears the CompositeDisposable so that it is no longer observing once the Activity has stopped.

Build and run the app on a device or emulator. It should function the same as before.

Though it may seem like all the changes you’ve made so far haven’t accomplished much, you’ve actually done quite a bit to increase the separation of concerns for this part of the app! Just open up the MainContract to see evidence of this:

interface PresenterInterface {  
  fun getMyMoviesList()  
  fun stop()  
}  

interface ViewInterface {  
  fun displayMovies(movieList: List<Movie>)  
  fun displayNoMovies()
  fun displayMessage(message: String)  
  fun displayError(message: String)  
}

The methods in this contract clearly delineate the roles of the Presenter and View: While the Presenter is responsible for interacting with the Model to get the movie list, the View is only responsible for hiding and displaying UI as directed by the Presenter.

Note: To easily navigate to actual implementations of interface methods in Android Studio, use CMD + OPTION + B on Mac or CTRL + ALT + B on Windows.

Deleting movies

Next, consider the flow for deleting movies. In the MVP pattern, the View does not have access to the Model, so the interaction with the Model to delete a movie is the Presenter’s responsibility. Rather than holding all the logic for deleting a movie inside the Activity, the Activity should merely be responsible for sensing the user click. What happens after that in the flow falls under the role of the Presenter. The new flow should look like this:

Flow for deleting movies.
Flow for deleting movies.

Once again, the flow breaks down into three steps:

  • Step One: The View notifies the Presenter through the PresenterInterface that the user would like to delete a given Movie.
  • Step Two: The Presenter tells the Model to delete a given Movie.
  • Step Three: If the Movie was successfully deleted, the Presenter notifies the View of the deletion through the ViewInterface.

To implement Step One, open MainActivity.kt and in the MainActivity’s onOptionsItemSelected, replace the contents of the method with the following:

override fun onOptionsItemSelected(item: MenuItem): Boolean {  
  if (item.itemId == R.id.deleteMenuItem) {  
    mainPresenter.onDeleteTapped(adapter.selectedMovies)  
  }  

  return super.onOptionsItemSelected(item)  
}

In the above snippet, the View simply informs the Presenter that the Delete button has been tapped, and it passes in the HashSet of movies to be deleted.

Next, open MainContract.kt and add the interface method fun onDeleteTapped(selectedMovies: HashSet<*>) to MainContract.PresenterInterface. Then, implement the method in the Presenter like so:

override fun onDeleteTapped(selectedMovies: HashSet<*>) {
  for (movie in selectedMovies) {
    dataSource.delete(movie as Movie)
  }
  if (selectedMovies.size == 1) {
    viewInterface.displayMessage("Movie deleted")
  } else if (selectedMovies.size > 1) {
    viewInterface.displayMessage("Movies deleted")
  }
}

Upon receiving the HashSet of movies, the Presenter performs Step Two of the flow, interacting with the Model to delete each movie in the HashSet. Then, there is some logic to determine what the View should show in a Toast. Depending on the number of deleted movies, the Presenter performs Step Three of the flow which is to notify the View of the deletion.

At this point, you can clean up the MainActivity by removing the instance variable dataSource, as the View does not interact with the Model in the MVP pattern.

There should be no more references to the dataSource instance within MainActivity, because all interaction with the Model, including retrieval and deletion, has been moved into the MainPresenter.

Scroll through the methods in MainActivity at this point and notice that the methods that remain only handle displaying views, listening to clicks, and navigating to a new screen. Thus, there is not much value in writing tests for the View, as you can assume the actual act of displaying a Toast or opening a new screen will work as expected.

Any logic that is worth testing is moved into the Presenter, which you will thoroughly unit test in the next chapter. Build and Run your app; deletion should work just the same as before.

The Add Movie screen

The Add Movie screen displays two text inputs for the user to fill out with the movie information, with an Add Movie button to submit the movie data. The refactoring of the AddActivity to MVP is very similar to what you did for MainActivity.

Create an AddMoviePresenter.kt class and an AddMovieContract.kt class under the add subpackage you made earlier. Add these interfaces to the AddMovieContract:

class AddMovieContract {  
  interface PresenterInterface {  
    fun addMovie(title: String, releaseDate: String, posterPath: String)  
  }  

  interface ViewInterface {  
    fun returnToMain()  
    fun displayMessage(message: String)
    fun displayError(message: String)
  }  
}

Then, create a new AddMoviePresenter.kt file and add the following:

class AddMoviePresenter(
	private var viewInterface: AddMovieContract.ViewInterface,
	private var dataSource: LocalDataSource) : AddMovieContract.PresenterInterface {

  override fun addMovie(
		title: String,
		releaseDate: String,
		posterPath: String) {
  }
}

The AddMoviePresenter class implements the AddMovieContract.PresenterInterface and takes in a reference to the AddMovieContract.ViewInterfaceand a reference to the Model, the LocalDataSource class.

The reason for injecting these two dependencies into the constructor is to unit test AddMoviePresenter more easily. During unit tests you will pass in mock objects into the constructor to make it easier to unit test just the AddMoviePresenter class itself rather than its dependencies. For now, you’ll leave the addMovie method empty; you’ll flesh that out in a few steps.

Next, open AddMovieActivity.kt, and have the class implement the AddMovieContract.ViewInterface like so:

class AddMovieActivity : AppCompatActivity(), AddMovieContract.ViewInterface {
  ...
}

Because AddMovieActivity now implements AddMovieContract.ViewInterface , prepend the override keyword to the displayMessage(message: String) and displayError(message: String) methods of AddMovieActivity:

override fun displayMessage(message: String) {
  Toast.makeText(this@AddMovieActivity, string, Toast.LENGTH_LONG).show()
}

override fun displayError(message: String) {
  displayMessage(message)
}

Then, add the following to AddMovieActivity:

private lateinit var addMoviePresenter: AddMoviePresenter

fun setupPresenter() {  
  val dataSource = LocalDataSource(application)  
  addMoviePresenter =  AddMoviePresenter(this, dataSource)  
}

Call this setupPresenter() method inside the AddMovieActivity’s onCreate() to instantiate the Presenter and pass a local instance of the Model to the Presenter.

Adding movies

Now, you are ready to move the presentation logic surrounding the adding of movies out of the View and into the Presenter. Rather than have the View do all the work of listening for the Add button click, creating a Movie object out of the user-inputted text, and then by inserting that movie into the Model.

The new flow should move the non-UI-related responsibilities to the Presenter to look like this:

Flow for adding movies.
Flow for adding movies.

In the same three step process you’ve seen before, the flow breaks down as follows:

  • Step One: The View notifies the Presenter through the PresenterInterface that the user would like to add a new Movie to their list.
  • Step Two: The Presenter inserts the Movie directly into the Model. In the use case where the data that is passed from the View to the Presenter for insertion, the Presenter might use this opportunity to perform validation on the supplied data.
  • Step Three: The Presenter notifies the View through the ViewInterface that the Movie has been added, and that it should transition back to the Main View.

To implement Step One, open AddMovieActivity and replace the onClickAddMovie(view: View) method with this one:

fun onClickAddMovie(view: View) {  
  val title = titleEditText.text.toString()  
  val releaseDate = releaseDateEditText.text.toString()  
  val posterPath = if (movieImageView.tag != null) movieImageView.tag.toString() else ""  

  addMoviePresenter.addMovie(title, releaseDate, posterPath)  
}

The View gathers all the input from the user, including the title, release date and poster path, and it passes them to the Presenter. Then, the AddMoviePresenter determines what to do with the inputted data.

Open AddMoviePresenter.kt and fill out the addMovie() method like this:

override fun addMovie(title: String, releaseDate: String, posterPath: String) {  
  //1
  if (title.isEmpty()) {  
    viewInterface.displayError("Movie title cannot be empty")  
  } else {  
    //2
    val movie = Movie(title, releaseDate, posterPath)  
    dataSource.insert(movie)  
    viewInterface.returnToMain()  
  }  
}
  1. If the user did not input the movie title, then show an error message.
  2. Otherwise, if at least the title of the movie was provided, the Presenter will create a new Movie object and ask the Model, the LocalDataSource, to insert that movie into the local database in Step Two of the flow. After that, in Step Three, the Presenter asks the View to finish the Activity and return to the main screen.

Open AddMovieActivity.kt once again, and add this returnToMain() method to the AddMovieActivity class:

override fun returnToMain() {  
  setResult(Activity.RESULT_OK)  
  finish()  
}

Because only a class that extends Activity knows how to finish() itself, the handling of navigation is therefore left to the View.

As a final step, remove the LocalDataSource instance variable initialization and instantiation from the AddMovieActivity to break the connection between the View and the Model, as is required in the MVP pattern.

Build and run the app to verify that everything still works properly.

The Search Movie screen

Recall that the Search Movie screen displays the list of search results for the movie title query that was passed in through the Intent. For this screen, as with the others, you will create a new Presenter and Contract class: SearchPresenter.kt and SearchContract.kt. First add the Contract class:

class SearchContract {

  interface PresenterInterface {
    fun getSearchResults(query: String)
    fun stop()
  }

  interface ViewInterface {
    fun displayResult(tmdbResponse: TmdbResponse)
    fun displayMessage(message: String)
    fun displayError(message: String)
  }
}

The Contract class between the Presenter and the View clearly differentiates the roles of the two interfaces. The role of the View is simply to display views, while the responsibility of communicating with the Model to fetch the search results falls on the Presenter.

Open SearchPresenter.kt and add the following class:

class SearchPresenter(
	private var viewInterface: SearchContract.ViewInterface,
	private var dataSource: RemoteDataSource) : SearchContract.PresenterInterface {
  private val TAG = "SearchPresenter"
  ...
}

The SearchPresenter needs references to both the ViewInterface and the RemoteDataSource passed in so that it can interact with the RemoteDataSource to fetch results and then tell the View what to display afterward. By injecting these dependencies through the SearchPresenter’s constructor, it will allow for mock objects to be passed in for the View and RemoteDataSource when writing unit tests later.

Next, open SearchActivity.kt and have it implement SearchContract.ViewInterface like so:

class SearchActivity : AppCompatActivity(), SearchContract.ViewInterface {
  ...
}

Just like you did with the other screens, prepend the search screen’s displayResult(tmdbResponse: TmdbResponse) and displayMessage(message: String) methods with the override keyword in the SearchActivity so that the SearchActivity conforms to the contract for SearchContract.ViewInterface.

Next, add a searchPresenter instance variable to the SearchActivity and add a method that sets up the Presenter:

private lateinit var searchPresenter: SearchPresenter

private fun setupPresenter() {
  val dataSource = RemoteDataSource()
  searchPresenter = SearchPresenter(this, dataSource)
}

To instantiate the SearchPresenter, the SearchActivity is passed in through this keyword, and the RemoteDataSource is also passed in. Be sure to call setupPresenter() in the SearchActivity’s onCreate(). Now that your Presenter and View are properly connected, you can move the logic of fetching search results out of the View and into the Presenter. Here is the MVP way to execute the flow:

Flow for getting search results.
Flow for getting search results.

To implement Step One in this flow, add a call to the Presenter’s searchPresenter.getSearchResults() to onStart() in SearchActivity like this:

override fun onStart() {
  super.onStart()
  progressBar.visibility = VISIBLE
  searchPresenter.getSearchResults(query)
}

Next, open SearchPresenter.kt and add the following, moved from SearchActivity:

private val compositeDisposable = CompositeDisposable()

//1
val searchResultsObservable: (String) -> Observable<TmdbResponse> = { query -> dataSource.searchResultsObservable(query) }

//2
val observer: DisposableObserver<TmdbResponse>
  get() = object : DisposableObserver<TmdbResponse>() {

    override fun onNext(@NonNull tmdbResponse: TmdbResponse) {
      Log.d(TAG, "OnNext" + tmdbResponse.totalResults)
      viewInterface.displayResult(tmdbResponse)
    }

    override fun onError(@NonNull e: Throwable) {
      Log.d(TAG, "Error fetching movie data.", e)
      viewInterface.displayError("Error fetching movie data.")
    }

    override fun onComplete() {
      Log.d(TAG, "Completed")
    }
  }

//3
override fun getSearchResults(query: String) {
  val searchResultsDisposable = searchResultsObservable(query)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribeWith(observer)

  compositeDisposable.add(searchResultsDisposable)
}
  1. The searchResultsObservable is an observable for the TmdbResponse from the dataSource, taking a movie title query as an input. This code is the same as before, except now the Presenter will be the one interacting with the dataSource to get the API response, which is why we passed in the dataSource to the Presenter through the constructor.
  2. Inside the observer, the Presenter determines how to consume the response it receives, depending on if it was a success or error. What has changed in this code is that we must call the viewInterface to perform responses to each of these scenarios. The Presenter does not know how to display the response or errors so it delegates those tasks to the viewInterface. If there is a successful response to display, the Presenter asks the viewInterface to display the result. If there is an error, the Presenter asks the viewInterface to display an error Toast.
  3. The getSearchResults(query: String) connects the observer to the searchResultsObservable so that it can begin observing. The method is exactly the same as before, except now it belongs to the Presenter.

In the code above, the Presenter performs Step Two and Step Three of the flow diagram, setting up the observable to get the search results from the Model and, then upon receiving them, instructing the View to display the results.

Again, the CompositeDisposable must be stopped when the Activity stops, so in the Presenter, remember to add this method:

override fun stop() {
  compositeDisposable.clear()
}

Open SearchActivity.kt again and call the stop() method of the Presenter in the activity onStop() like this:

override fun onStop() {
  super.onStop()
  searchPresenter.stop()
}

Finally, clean up the SearchActivity by removing the instance variable dataSource. There should be no more references to the dataSource instance within SearchActivity, because all interaction with the Model to get search results has been moved into the SearchPresenter.

Build and run your app to confirm that searching for a movie still works as expected.

Key points

  • In the Model View Presenter pattern, each View interacts with an associated Presenter class.
  • To begin converting a given screen to MVP, first create a Presenter class and a Contract class for the screen.
  • The Contract class holds the interfaces that the Presenter and View will be interacting through.
  • In the View’s onCreate(), call a method to setup the Presenter.
  • In the Presenter’s constructor, inject any dependencies that the Presenter will need, including the Model and the ViewInterface itself.
  • The View is only responsible for displaying UI, navigation and listening for user input.
  • Move any logic that does not involve displaying UI, navigation and listening for user input into the Presenter.
  • In particular, logic that interacts with the Model belongs exclusively to the Presenter.
  • Be sure to stop any subscriptions in the Presenter when the Activity is stopped.

Where to go from here?

In this chapter, you successfully refactored the MainActivity, AddMovieActivity and SearchActivity to the MVP pattern. After adding Presenter and Contract files to each of your Views, here is what your project directory should look like now:

As you were building and running your project throughout his chapter, you confirmed that the app ran exactly the same as before. A user would not notice any difference with the app now refactored into MVP, as the functionality has not changed. What has changed in each of the screens of the app by refactoring to MVP is the extent to which each type of class is now focused on their own designated tasks, achieving a greater degree of separation of concerns, as well as greater decoupling.

However, the biggest change that converting to MVP has had on this project has yet to be demonstrated: improved testability. As you will see in the next chapter, keeping the Android framework-specific code out of the Presenter allows the Presenter to be completely unit testable. Now you are ready to write some tests!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.