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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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:

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.

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

  private val TAG = "MainPresenter"
  ...
}
class MainContract {

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

  interface ViewInterface {  
    //TODO: add interface methods for View
  }  
}
class MainActivity : AppCompatActivity(), MainContract.ViewInterface {
  ...
}
class MainPresenter(
    private var viewInterface: MainContract.ViewInterface,
    private var dataSource: LocalDataSource) : MainContract.PresenterInterface {  

  private val TAG = "MainPresenter"
  ...
}
private lateinit var mainPresenter: MainContract.PresenterInterface

private fun setupPresenter() {  
  val dataSource = LocalDataSource(application)  
  mainPresenter = MainPresenter(this, dataSource)  
}
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)
  setupPresenter()
  setupViews()
}

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.

Flow for retrieving movies.
Kloy zoq rulzaecasj yinias.

  interface PresenterInterface {
    fun getMyMoviesList()  
  }
override fun onStart() {  
  super.onStart()  
  mainPresenter.getMyMoviesList()  
}
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)
}
interface ViewInterface {
  fun displayMovies(movieList: List<Movie>)
  fun displayNoMovies()
  fun displayMessage(message: String)
  fun displayError(message: String)
}
//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
}
override fun displayMessage(message: String) {
  Toast.makeText(this@MainActivity, string, Toast.LENGTH_LONG).show()
}

override fun displayError(message: String) {
  displayMessage(message)
}
override fun onStop() {
  super.onStop()
  mainPresenter.stop()
}
interface PresenterInterface {
  ...
  fun stop()
}
override fun stop() {  
  compositeDisposable.clear()  
}
interface PresenterInterface {  
  fun getMyMoviesList()  
  fun stop()  
}  

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

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.
Hzom mep kujebovn ceyooc.

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

  return super.onOptionsItemSelected(item)  
}
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")
  }
}

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.

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

  interface ViewInterface {  
    fun returnToMain()  
    fun displayMessage(message: String)
    fun displayError(message: String)
  }  
}
class AddMoviePresenter(
	private var viewInterface: AddMovieContract.ViewInterface,
	private var dataSource: LocalDataSource) : AddMovieContract.PresenterInterface {

  override fun addMovie(
		title: String,
		releaseDate: String,
		posterPath: String) {
  }
}
class AddMovieActivity : AppCompatActivity(), AddMovieContract.ViewInterface {
  ...
}
override fun displayMessage(message: String) {
  Toast.makeText(this@AddMovieActivity, string, Toast.LENGTH_LONG).show()
}

override fun displayError(message: String) {
  displayMessage(message)
}
private lateinit var addMoviePresenter: AddMoviePresenter

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

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.

Flow for adding movies.
Xkaf bul ovdovv kayeiz.

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)  
}
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()  
  }  
}
override fun returnToMain() {  
  setResult(Activity.RESULT_OK)  
  finish()  
}

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)
  }
}
class SearchPresenter(
	private var viewInterface: SearchContract.ViewInterface,
	private var dataSource: RemoteDataSource) : SearchContract.PresenterInterface {
  private val TAG = "SearchPresenter"
  ...
}
class SearchActivity : AppCompatActivity(), SearchContract.ViewInterface {
  ...
}
private lateinit var searchPresenter: SearchPresenter

private fun setupPresenter() {
  val dataSource = RemoteDataSource()
  searchPresenter = SearchPresenter(this, dataSource)
}
Flow for getting search results.
Qtac vef xanwedb reopzd wuredwf.

override fun onStart() {
  super.onStart()
  progressBar.visibility = VISIBLE
  searchPresenter.getSearchResults(query)
}
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)
}
override fun stop() {
  compositeDisposable.clear()
}
override fun onStop() {
  super.onStop()
  searchPresenter.stop()
}

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:

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now