Android VIPER Tutorial

In this tutorial, you’ll become familiar with the various layers of the VIPER architecture pattern and see how to keep your app modules clean and independent. By Pablo L. Sordo Martinez.

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

App modules and entities

In this tutorial, every module consists of a contract and several associated classes which implement the various VIPER layers. The contract describes which VIPER layers must be implemented in the module and the actions the layers will perform.

Have a quick look at the contract corresponding to the Splash Module:

interface SplashContract {
  interface View {
    fun finishView()
  }

  interface Presenter {
    // Model updates
    fun onViewCreated()
    fun onDestroy()
  }
}

You can see there are two layers, defined as interfaces, that must be implemented: View and Presenter. Obviously, the functions declared inside the interfaces will be defined at some point in the code.

Main Module

View

Starting with MainActivity, make it extend BaseActivity() (instead of AppCompatActivity()), and implement the interface MainContract.View.

class MainActivity : BaseActivity(), MainContract.View {

After that, you will be required to implement some missing members. Hit Ctrl+I, and you will see that only one method corresponds to BaseActivity(), whereas the rest belong to MainContract.View.

MainActivity missing members

Before populating these methods, add a few properties:

private var presenter: MainContract.Presenter? = null 
private val toolbar: Toolbar by lazy { toolbar_toolbar_view } 
private val recyclerView: RecyclerView by lazy { rv_jokes_list_activity_main } 
private val progressBar: ProgressBar by lazy { prog_bar_loading_jokes_activity_main } 

You’ll need to hit Alt+Enter on PC or Option+Return on Mac to pull in the imports, and be sure to use the synthetic Kotlin Android Extensions for the view binding. Note that the presenter corresponds to the same module as the current view.

Now, you can fill the overridden functions in as follows:

override fun getToolbarInstance(): Toolbar? = toolbar

This simply returns the Toolbar instance present in the layout.

Add overrides for the loading functions:

override fun showLoading() {
  recyclerView.isEnabled = false
  progressBar.visibility = View.VISIBLE
}

override fun hideLoading() {
  recyclerView.isEnabled = true
  progressBar.visibility = View.GONE
}

The above two functions handle the data loading, showing and hiding the ProgressBar instance included in the layout.

Next, override showInfoMessage using the toast() method from Anko Commons.

override fun showInfoMessage(msg: String) {
  toast(msg)
}

This is a simple function to show some info to the user.

Add an override for publishDataList():

override fun publishDataList(data: List<Joke>) {
  (recyclerView.adapter as JokesListAdapter).updateData(data)
}

This last function updates the RecyclerView instance data.

Do not forget to initialize the presenter (you will define it shortly) and to configure the RecyclerView; add the following in onCreate():

presenter = MainPresenter(this)
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerView.adapter = JokesListAdapter({ joke -> presenter?.listItemClicked(joke) }, null)

Finally, it is common when doing VIPER, that you inform the presenter when the view is visible, and make the instance null when the Activity is being destroyed. Add the following two lifecycle method overrides:

override fun onResume() {
  super.onResume()
  presenter?.onViewCreated()
}

override fun onDestroy() {
  presenter?.onDestroy()
  presenter = null
    super.onDestroy()
}

Note: you can use a Dependency Injection approach, such as Dagger 2, Kodein, or Koin to avoid making instances null before finishing in order to prevent memory leaks.

Presenter

Now you can define MainPresenter as follows, and place it in the presenter folder:

class MainPresenter(private var view: MainContract.View?)
    : MainContract.Presenter, MainContract.InteractorOutput {   // 1

  private var interactor: MainContract.Interactor? = MainInteractor()   // 2

  override fun listItemClicked(joke: Joke?) {   // 3
  }

  override fun onViewCreated() {   // 4
  }

  override fun onQuerySuccess(data: List<Joke>) {   // 5
  }

  override fun onQueryError() {
  }

  override fun onDestroy() {   // 6
  }
}

To define the implementation, you have to take into account that:

  1. The class implements two interfaces declared in the MainContract: Presenter, which commands the whole module, and InteractorOutput, which allows you to define actions in response to what the Interactor returns.
  2. You will need an Interactor instance to perform actions of interest.
  3. When this function is called, the application will navigate to another screen (Detail Module) passing any data of interest; thus, it will be using the Router.
  4. This callback defines what will happen when the view is finally loaded. Normally, you define here anything which happens automatically, with no user interaction. In this case, you will try to fetch data from the datasource through the REST API.
  5. This function and the following will define what happens when the database query is addressed.
  6. As for the presenter in the view, you need to make the presenter properties null in order to avoid any trouble when the system kills the module.

Setting aside the Router for further implementation, you can actually fill in the rest of functions.

For onQuerySuccess(data: List), add

view?.hideLoading()
view?.publishDataList(data)

and for onQueryError(),

view?.hideLoading()
view?.showInfoMessage("Error when loading data")

You simply handle the success or the error of the data query.

In onDestroy(), add

view = null
interactor = null

You make the properties null, as you did in the view layer.

Interactor

For onViewCreated() in the presenter, you want to query data from the remote datasource once the view loads. You need to first create the MainInteractor class in the interactor package.

class MainInteractor : MainContract.Interactor {   // 1

  companion object {
    val icndbUrl = "https://api.icndb.com/jokes"
  }

  override fun loadJokesList(interactorOutput: (result: Result<Json, FuelError>) -> Unit) {   // 2
    icndbUrl.httpPost().responseJson { _, _, result ->   // 3
      interactorOutput(result)
    }
  }
}

Pull in dependencies via Alt+Enter on PC and Option+Return on Mac using import statements that begin with com.github.kittinunf. Of note in MainInteractor are the following:

  1. Do not forget to make the class implement the proper interface, from the module contract.
  2. loadJokesList() is a function which requires a lambda as input argument; this is technically the same as having a typical Java Callback.
  3. In order to query data, the application uses Fuel, which is a Kotlin/Android library that allows easy and asynchronous networking. Another alternative would be using the well-known Retrofit. The query result is directly used by the lambda as input argument.

Do you remember the onViewCreated() function in MainPresenter that you left empty in the previous section? It is time now to fill it in.

override fun onViewCreated() {
  view?.showLoading()
  interactor?.loadJokesList { result ->
    when (result) {
      is Result.Failure -> {
        this.onQueryError()
      }
      is Result.Success -> {
        val jokesJsonObject = result.get().obj()

        val type = object : TypeToken<List<Joke>>() {}.type
        val jokesList: List<Joke> =
            Gson().fromJson(jokesJsonObject.getJSONArray("value").toString(), type)

        this.onQuerySuccess(jokesList)
      }
    }
  }
}

As you see, you only have to handle the possible results of the query. Bear in mind that Result.Success returns a JSON object that has to be parsed somehow; in this case, you’re using Gson.

Note: remember that when parsing a JSON stream, you first need to know how data is organized. In this case, “value” is the root keyword of the array.

Build and run the app, and you’ll be greeted with a set of illuminating facts: